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;
015:
016: import java.beans.PropertyChangeEvent;
017: import java.beans.PropertyChangeListener;
018: import java.util.ArrayList;
019: import java.util.HashMap;
020:
021: import javax.swing.text.BadLocationException;
022: import javax.swing.text.JTextComponent;
023: import javax.swing.text.Position;
024:
025: /**
026: * Word matching support enables to fill in the rest of the word when knowing
027: * the begining of the word. It is capable to search either only in current file
028: * or also in several or all open files.
029: *
030: * @author Miloslav Metelka
031: * @version 1.00
032: */
033:
034: public class WordMatch extends FinderFactory.AbstractFinder implements
035: SettingsChangeListener, PropertyChangeListener {
036:
037: private static final Object NULL_DOC = new Object();
038:
039: /** Mapping of kit class to document with the static word */
040: private static final HashMap staticWordsDocs = new HashMap();
041:
042: /**
043: * First part of matching word expressed as char[]. Status of word matching
044: * support can be tested by looking if this variable is null. If it is, word
045: * matching was reset and it's not initialized yet.
046: */
047: char[] baseWord;
048:
049: /** Found characters are accumulated here */
050: char[] word = new char[20];
051:
052: /** Last word returned */
053: String lastWord;
054:
055: /** Previous word returned */
056: String previousWord;
057:
058: /** Current index in word */
059: int wordLen;
060:
061: /** HashMap for already matched words */
062: StringMap wordsMap = new StringMap();
063:
064: /** ArrayList holding already found words and their positions. */
065: ArrayList wordInfoList = new ArrayList();
066:
067: /**
068: * Current index in word match vector. Reaching either first or last index
069: * of vector means searching backward or forward respectively from position
070: * stored in previous vector's element.
071: */
072: int wordsIndex;
073:
074: /** Current search direction */
075: boolean forwardSearch;
076:
077: /** Pointer to editorUI instance */
078: EditorUI editorUI;
079:
080: /** Whether the search should be wrapped */
081: boolean wrapSearch;
082:
083: /** Search with case matching */
084: boolean matchCase;
085:
086: /** Search using smart case */
087: boolean smartCase;
088:
089: /**
090: * This is the flag that really says whether the search is matching case or
091: * not. The value is (smartCase ? (is-there-capital-in-base-word?) :
092: * matchCase).
093: */
094: boolean realMatchCase;
095:
096: /**
097: * Whether the match should be reported when word is found which is only one
098: * char long.
099: */
100: boolean matchOneChar;
101:
102: /**
103: * Maximum lenght in chars of the search area. If the number is zero, no
104: * search is performed except the static words.
105: */
106: int maxSearchLen;
107:
108: /** Current count of documents where the search was performed */
109: int searchLen;
110:
111: /** Document where to start from */
112: BaseDocument startDoc;
113:
114: /** Construct new word match over given view manager */
115: public WordMatch(EditorUI editorUI) {
116: this .editorUI = editorUI;
117:
118: Settings.addSettingsChangeListener(this );
119:
120: synchronized (editorUI.getComponentLock()) {
121: // if component already installed in EditorUI simulate installation
122: JTextComponent component = editorUI.getComponent();
123: if (component != null) {
124: propertyChange(new PropertyChangeEvent(editorUI,
125: EditorUI.COMPONENT_PROPERTY, null, component));
126: }
127:
128: editorUI.addPropertyChangeListener(this );
129: }
130: }
131:
132: /**
133: * Called when settings were changed. The method is called by editorUI when
134: * settings were changed and from constructor.
135: */
136: public void settingsChange(SettingsChangeEvent evt) {
137: if (evt != null) { // real change event
138: staticWordsDocs.clear();
139: }
140:
141: Class kitClass = Utilities.getKitClass(editorUI.getComponent());
142: if (kitClass != null) {
143: maxSearchLen = SettingsUtil.getInteger(kitClass,
144: SettingsNames.WORD_MATCH_SEARCH_LEN,
145: Integer.MAX_VALUE);
146: wrapSearch = SettingsUtil.getBoolean(kitClass,
147: SettingsNames.WORD_MATCH_WRAP_SEARCH, true);
148: matchOneChar = SettingsUtil.getBoolean(kitClass,
149: SettingsNames.WORD_MATCH_MATCH_ONE_CHAR, true);
150: matchCase = SettingsUtil.getBoolean(kitClass,
151: SettingsNames.WORD_MATCH_MATCH_CASE, false);
152: smartCase = SettingsUtil.getBoolean(kitClass,
153: SettingsNames.WORD_MATCH_SMART_CASE, false);
154: }
155: }
156:
157: public void propertyChange(PropertyChangeEvent evt) {
158: String propName = evt.getPropertyName();
159:
160: if (EditorUI.COMPONENT_PROPERTY.equals(propName)) {
161: JTextComponent component = (JTextComponent) evt
162: .getNewValue();
163: if (component != null) { // just installed
164:
165: settingsChange(null);
166:
167: } else { // just deinstalled
168: // component = (JTextComponent)evt.getOldValue();
169:
170: }
171:
172: }
173: }
174:
175: /**
176: * Clear word matching, so that it forgots the remembered matching words.
177: */
178: public synchronized void clear() {
179: if (baseWord != null) {
180: baseWord = null;
181: wordsMap.clear();
182: wordInfoList.clear();
183: wordsIndex = 0;
184: searchLen = maxSearchLen;
185: }
186: }
187:
188: /** Reset this finder before each search */
189: public void reset() {
190: super .reset();
191: wordLen = 0;
192: }
193:
194: /**
195: * Find next matching word and replace it on current cursor position
196: *
197: * @param forward
198: * in which direction should the search be done
199: */
200: public synchronized String getMatchWord(int startPos,
201: boolean forward) {
202: int listSize = wordInfoList.size();
203: boolean searchNext = (listSize == 0)
204: || (wordsIndex == (forward ? (listSize - 1) : 0));
205: startDoc = (BaseDocument) editorUI.getComponent().getDocument();
206: String ret = null;
207:
208: // initialize base word if necessary
209: if (baseWord == null) {
210: try {
211: String baseWordString = Utilities.getIdentifierBefore(
212: startDoc, startPos);
213: if (baseWordString == null) {
214: baseWordString = ""; // NOI18N
215: }
216: lastWord = baseWordString;
217: baseWord = baseWordString.toCharArray();
218:
219: WordInfo info = new WordInfo(baseWordString, startDoc
220: .createPosition(startPos - baseWord.length),
221: startDoc);
222: wordsMap.put(info.word, info);
223: wordInfoList.add(info);
224: } catch (BadLocationException e) {
225: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
226: e.printStackTrace();
227: }
228: }
229: if (smartCase && !matchCase) {
230: realMatchCase = false;
231: for (int i = 0; i < baseWord.length; i++) {
232: if (Character.isUpperCase(baseWord[i])) {
233: realMatchCase = true;
234: }
235: }
236: } else {
237: realMatchCase = matchCase;
238: }
239: // make lowercase if not matching case
240: if (!realMatchCase) {
241: for (int i = 0; i < baseWord.length; i++) {
242: baseWord[i] = Character.toLowerCase(baseWord[i]);
243: }
244: }
245: }
246:
247: // possibly search next word
248: if (searchNext) {
249: try {
250: // determine start document and position
251: BaseDocument doc; // actual document
252: int pos; // actual position
253: if (listSize > 0) {
254: WordInfo info = (WordInfo) wordInfoList
255: .get(wordsIndex);
256: doc = info.doc;
257: pos = info.pos.getOffset();
258: if (forward) {
259: pos += info.word.length();
260: }
261: } else {
262: doc = startDoc;
263: pos = startPos;
264: }
265:
266: // search for next occurence
267: while (doc != null) {
268: if (doc.getLength() > 0) {
269: int endPos;
270: if (doc == startDoc) {
271: if (forward) {
272: endPos = (pos >= startPos) ? -1
273: : startPos;
274: } else { // bwd
275: endPos = (pos == -1 || pos > startPos) ? startPos
276: : 0;
277: }
278: } else { // not starting doc
279: endPos = -1;
280: }
281:
282: this .forwardSearch = !(!forward && (doc == startDoc));
283: int foundPos = doc.find(this , pos, endPos);
284: if (foundPos != -1) { // found
285: if (forward) {
286: wordsIndex++;
287: }
288: WordInfo info = new WordInfo(new String(
289: word, 0, wordLen), doc
290: .createPosition(foundPos), doc);
291: wordsMap.put(info.word, info);
292: wordInfoList.add(wordsIndex, info);
293: previousWord = lastWord;
294: lastWord = info.word;
295: return lastWord;
296: }
297: if (doc == startDoc) {
298: if (forward) {
299: pos = 0;
300: if (endPos != -1 || !wrapSearch) {
301: doc = getNextDoc(doc);
302: }
303: } else { // bwd
304: if (pos == -1 || !wrapSearch) {
305: doc = getNextDoc(doc);
306: pos = 0;
307: } else {
308: pos = -1; // stay on the same document
309: }
310: }
311: } else { // not starting doc
312: doc = getNextDoc(doc);
313: pos = 0;
314: }
315: } else { // empty document
316: doc = getNextDoc(doc);
317: pos = 0; // should be anyway
318: }
319: }
320: // Return null in this case
321: } catch (BadLocationException e) {
322: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
323: e.printStackTrace();
324: }
325: }
326: } else { // use word from the list
327: wordsIndex += (forward ? 1 : -1);
328: previousWord = lastWord;
329: lastWord = ((WordInfo) wordInfoList.get(wordsIndex)).word;
330: ret = lastWord;
331: }
332:
333: startDoc = null;
334: return ret;
335: }
336:
337: public String getPreviousWord() {
338: return previousWord;
339: }
340:
341: private void doubleWordSize() {
342: char[] tmp = new char[word.length * 2];
343: System.arraycopy(word, 0, tmp, 0, word.length);
344: word = tmp;
345: }
346:
347: private boolean checkWord() {
348: // check matching of one-char string
349: if (!matchOneChar && wordLen == 1) {
350: return false;
351: }
352:
353: // check word start
354: if (baseWord.length > 0) {
355: if (wordLen < baseWord.length) {
356: return false;
357: }
358: for (int i = 0; i < baseWord.length; i++) {
359: if (realMatchCase) {
360: if (word[i] != baseWord[i]) {
361: return false;
362: }
363: } else { // case-insensitive
364: if (Character.toLowerCase(word[i]) != baseWord[i]) {
365: return false;
366: }
367: }
368: }
369: }
370:
371: // check existing words
372: if (wordsMap.containsKey(word, 0, wordLen)) {
373: return false;
374: }
375: return true; // new word found
376: }
377:
378: public int find(int bufferStartPos, char buffer[], int offset1,
379: int offset2, int reqPos, int limitPos) {
380: int offset = reqPos - bufferStartPos;
381: if (forwardSearch) {
382: int limitOffset = limitPos - bufferStartPos - 1;
383: while (offset < offset2) {
384: char ch = buffer[offset];
385: boolean wp = startDoc.isIdentifierPart(ch);
386: if (wp) { // append the char
387: if (wordLen == word.length) {
388: doubleWordSize();
389: }
390: word[wordLen++] = ch;
391: }
392:
393: if (!wp) {
394: if (wordLen > 0) {
395: if (checkWord()) {
396: found = true;
397: return bufferStartPos + offset - wordLen;
398:
399: } else {
400: wordLen = 0;
401: }
402: }
403:
404: } else { // current char is word part
405: if (limitOffset == offset) {
406: if (checkWord()) {
407: found = true;
408: // differs in one char because current is part of
409: // word
410: return bufferStartPos + offset - wordLen
411: + 1;
412:
413: } else {
414: wordLen = 0;
415: }
416: }
417: }
418:
419: offset++;
420: }
421: } else { // bwd search
422: int limitOffset = limitPos - bufferStartPos;
423: while (offset >= offset1) {
424: char ch = buffer[offset];
425: boolean wp = startDoc.isIdentifierPart(ch);
426: if (wp) {
427: if (wordLen == word.length) {
428: doubleWordSize();
429: }
430: word[wordLen++] = ch;
431: }
432: if (!wp || (limitOffset == offset)) {
433: if (wordLen > 0) {
434: Analyzer.reverse(word, wordLen); // reverse word
435: // chars
436: if (checkWord()) {
437: found = true;
438: return (wp) ? bufferStartPos + offset + 1
439: : bufferStartPos + offset;
440: } else {
441: wordLen = 0;
442: }
443: }
444: }
445: offset--;
446: }
447: }
448: return bufferStartPos + offset;
449: }
450:
451: private BaseDocument getNextDoc(BaseDocument doc) {
452: if (doc == getStaticWordsDoc()) {
453: return null;
454: }
455: BaseDocument nextDoc = Registry.getLessActiveDocument(doc);
456: if (nextDoc == null) {
457: nextDoc = getStaticWordsDoc();
458: }
459: return nextDoc;
460: }
461:
462: private BaseDocument getStaticWordsDoc() {
463: Class kitClass = Utilities.getKitClass(editorUI.getComponent());
464: Object val = staticWordsDocs.get(kitClass);
465: if (val == NULL_DOC) {
466: return null;
467: }
468: BaseDocument doc = (BaseDocument) val;
469: if (doc == null) {
470: String staticWords = (String) Settings.getValue(kitClass,
471: SettingsNames.WORD_MATCH_STATIC_WORDS);
472: if (staticWords != null) {
473: doc = new BaseDocument(BaseKit.class, false); // don't add to
474: // registry
475: try {
476: doc.insertString(0, staticWords, null);
477: } catch (BadLocationException e) {
478: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
479: e.printStackTrace();
480: }
481: }
482: staticWordsDocs.put(kitClass, doc);
483: } else { // null static words
484: staticWordsDocs.put(kitClass, NULL_DOC);
485: }
486: }
487: return doc;
488: }
489:
490: /**
491: * Word match info - used in previous/next word matching. It contains info
492: * found word and next matching position.
493: */
494: private static final class WordInfo {
495:
496: public WordInfo(String word, Position pos, BaseDocument doc) {
497: this .word = word;
498: this .pos = pos;
499: this .doc = doc;
500: }
501:
502: /** Found word */
503: String word;
504:
505: /**
506: * Position of the word in document. Positions are used so that the
507: * marks are removed when they are no longer necessary.
508: */
509: Position pos;
510:
511: /** Document where the word resides */
512: BaseDocument doc;
513:
514: public boolean equals(Object o) {
515: if (this == o) {
516: return true;
517: }
518: if (o instanceof WordMatch) {
519: WordMatch wm = (WordMatch) o;
520: return Analyzer.equals(word, wm.word, 0, wm.wordLen);
521: }
522: if (o instanceof WordInfo) {
523: return word.equals(((WordInfo) o).word);
524: }
525: if (o instanceof String) {
526: return word.equals(o);
527: }
528: return false;
529: }
530:
531: public int hashCode() {
532: return word.hashCode();
533: }
534:
535: public String toString() {
536: return "{word='" + word + "', pos=" + pos.getOffset() // NOI18N
537: + ", doc=" + Registry.getID(doc) + "}"; // NOI18N
538: }
539:
540: }
541:
542: public String toString() {
543: return "baseWord="
544: + ((baseWord != null) ? ("'" + baseWord.toString() + "'") // NOI18N
545: : "null")
546: + ", wrapSearch="
547: + wrapSearch // NOI18N
548: + ", matchCase="
549: + matchCase
550: + ", smartCase="
551: + smartCase // NOI18N
552: + ", matchOneChar=" + matchOneChar
553: + ", maxSearchLen="
554: + maxSearchLen // NOI18N
555: + ", wordsMap=" + wordsMap + "\nwordInfoList="
556: + wordInfoList // NOI18N
557: + "\nwordsIndex=" + wordsIndex; // NOI18N
558: }
559:
560: }
|