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.editor.ext;
043:
044: import java.awt.EventQueue;
045: import java.awt.event.ActionListener;
046: import java.awt.event.ActionEvent;
047: import java.beans.PropertyChangeListener;
048: import java.beans.PropertyChangeEvent;
049: import javax.swing.Timer;
050: import javax.swing.SwingUtilities;
051: import javax.swing.text.JTextComponent;
052: import javax.swing.event.CaretListener;
053: import javax.swing.event.CaretEvent;
054: import javax.swing.event.DocumentListener;
055: import javax.swing.event.DocumentEvent;
056: import org.netbeans.editor.BaseDocument;
057: import org.netbeans.editor.Settings;
058: import org.netbeans.editor.SettingsChangeListener;
059: import org.netbeans.editor.SettingsChangeEvent;
060: import org.netbeans.editor.SettingsUtil;
061: import org.netbeans.editor.Utilities;
062: import org.netbeans.editor.WeakTimerListener;
063: import javax.swing.text.BadLocationException;
064: import javax.swing.event.ListSelectionListener;
065: import org.openide.util.NbBundle;
066: import org.openide.util.RequestProcessor;
067:
068: /**
069: * General Completion display formatting and services
070: *
071: * @author Miloslav Metelka
072: * @version 1.00
073: */
074:
075: public class Completion implements PropertyChangeListener,
076: SettingsChangeListener, ActionListener {
077:
078: /** Editor UI supporting this completion */
079: protected ExtEditorUI extEditorUI;
080:
081: /** Completion query providing query support for this completion */
082: private CompletionQuery query;
083:
084: /** Last result retrieved for completion. It can become null
085: * if the document was modified so the replacement position
086: * would be invalid.
087: */
088: private CompletionQuery.Result lastResult;
089:
090: private boolean keyPressed = false;
091:
092: /** Completion view component displaying the completion help */
093: private CompletionView view;
094:
095: /** Component (usually scroll-pane) holding the view and the title
096: * and possibly other necessary components.
097: */
098: private ExtCompletionPane pane;
099:
100: private JavaDocPane javaDocPane;
101:
102: private JDCPopupPanel jdcPopupPanel;
103:
104: private boolean autoPopup;
105:
106: private int autoPopupDelay;
107:
108: private int refreshDelay;
109:
110: private boolean instantSubstitution;
111:
112: Timer timer;
113: Timer docChangeTimer;
114:
115: private DocumentListener docL;
116: private CaretListener caretL;
117:
118: private PropertyChangeListener docChangeL;
119:
120: private int caretPos = -1;
121:
122: // old providers was called serialy from AWT, emulate it by RP queue
123: private static RequestProcessor serializingRequestProcessor;
124:
125: // sample property at static initialized, but allow dynamic disabling later
126: private static final String PROP_DEBUG_COMPLETION = "editor.debug.completion"; // NOI18N
127: private static final boolean DEBUG_COMPLETION = Boolean
128: .getBoolean(PROP_DEBUG_COMPLETION);
129:
130: // Every asynchronous task can be splitted into several subtasks.
131: // The task can between subtasks using simple test determine whether
132: // it was not cancelled.
133: // It emulates #28475 RequestProcessor enhancement request.
134: private CancelableRunnable cancellable = new CancelableRunnable() {
135: public void run() {
136: }
137: };
138: public boolean provokedByAutoPopup;
139:
140: public Completion(ExtEditorUI extEditorUI) {
141: this .extEditorUI = extEditorUI;
142:
143: // Initialize timer
144: timer = new Timer(0, new WeakTimerListener(this )); // delay will be set later
145: timer.setRepeats(false);
146:
147: docChangeTimer = new Timer(0, new WeakTimerListener(
148: new ActionListener() {
149: public void actionPerformed(ActionEvent e) {
150: refreshImpl(false); //??? why do not we batch them by posting it?
151: }
152: }));
153: docChangeTimer.setRepeats(false);
154:
155: // Create document listener
156: class CompletionDocumentListener implements DocumentListener {
157:
158: private void processTimer() {
159: docChangeTimer.stop();
160: setKeyPressed(true);
161: invalidateLastResult();
162: docChangeTimer.setInitialDelay(refreshDelay);
163: docChangeTimer.setDelay(refreshDelay);
164: docChangeTimer.start();
165: }
166:
167: public void insertUpdate(DocumentEvent evt) {
168: trace("ENTRY insertUpdate"); // NOI18N
169: processTimer();
170: }
171:
172: public void removeUpdate(DocumentEvent evt) {
173: trace("ENTRY removeUpdate"); // NOI18N
174: processTimer();
175: }
176:
177: public void changedUpdate(DocumentEvent evt) {
178: }
179: }
180: ;
181: docL = new CompletionDocumentListener();
182:
183: class CompletionCaretListener implements CaretListener {
184: public void caretUpdate(CaretEvent e) {
185: trace("ENTRY caretUpdate"); // NOI18N
186: if (!isPaneVisible()) {
187: // cancel timer if caret moved
188: cancelRequestImpl();
189: } else {
190: //refresh completion only if a pane is already visible
191: refreshImpl(true);
192: }
193: }
194: }
195: ;
196: caretL = new CompletionCaretListener();
197:
198: Settings.addSettingsChangeListener(this );
199:
200: synchronized (extEditorUI.getComponentLock()) {
201: // if component already installed in ExtEditorUI simulate installation
202: JTextComponent component = extEditorUI.getComponent();
203: if (component != null) {
204: propertyChange(new PropertyChangeEvent(extEditorUI,
205: ExtEditorUI.COMPONENT_PROPERTY, null, component));
206: }
207:
208: extEditorUI.addPropertyChangeListener(this );
209: }
210: }
211:
212: public void settingsChange(SettingsChangeEvent evt) {
213: Class kitClass = Utilities.getKitClass(extEditorUI
214: .getComponent());
215:
216: if (kitClass != null) {
217: autoPopup = SettingsUtil.getBoolean(kitClass,
218: ExtSettingsNames.COMPLETION_AUTO_POPUP,
219: ExtSettingsDefaults.defaultCompletionAutoPopup);
220:
221: autoPopupDelay = SettingsUtil
222: .getInteger(
223: kitClass,
224: ExtSettingsNames.COMPLETION_AUTO_POPUP_DELAY,
225: ExtSettingsDefaults.defaultCompletionAutoPopupDelay);
226:
227: refreshDelay = SettingsUtil.getInteger(kitClass,
228: ExtSettingsNames.COMPLETION_REFRESH_DELAY,
229: ExtSettingsDefaults.defaultCompletionRefreshDelay);
230:
231: instantSubstitution = SettingsUtil
232: .getBoolean(
233: kitClass,
234: ExtSettingsNames.COMPLETION_INSTANT_SUBSTITUTION,
235: ExtSettingsDefaults.defaultCompletionInstantSubstitution);
236:
237: }
238: }
239:
240: public void propertyChange(PropertyChangeEvent evt) {
241: String propName = evt.getPropertyName();
242:
243: if (ExtEditorUI.COMPONENT_PROPERTY.equals(propName)) {
244: JTextComponent component = (JTextComponent) evt
245: .getNewValue();
246: if (component != null) { // just installed
247:
248: settingsChange(null);
249:
250: BaseDocument doc = Utilities.getDocument(component);
251: if (doc != null) {
252: doc.addDocumentListener(docL);
253: }
254:
255: component.addCaretListener(caretL);
256: } else { // just deinstalled
257:
258: setPaneVisible(false);
259:
260: component = (JTextComponent) evt.getOldValue();
261:
262: BaseDocument doc = Utilities.getDocument(component);
263: if (doc != null) {
264: doc.removeDocumentListener(docL);
265: }
266:
267: if (component != null) {
268: component.removeCaretListener(caretL);
269: }
270: }
271:
272: } else if ("document".equals(propName)) { // NOI18N
273: if (evt.getOldValue() instanceof BaseDocument) {
274: ((BaseDocument) evt.getOldValue())
275: .removeDocumentListener(docL);
276: }
277: if (evt.getNewValue() instanceof BaseDocument) {
278: ((BaseDocument) evt.getNewValue())
279: .addDocumentListener(docL);
280: }
281:
282: }
283:
284: }
285:
286: public CompletionPane getPane() {
287: return (CompletionPane) getExtPane();
288: }
289:
290: public ExtCompletionPane getExtPane() {
291: if (pane == null) {
292: pane = new ScrollCompletionPane(extEditorUI);
293: }
294: return pane;
295: }
296:
297: protected CompletionView createView() {
298: return new ListCompletionView();
299: }
300:
301: public final CompletionView getView() {
302: if (view == null) {
303: view = createView();
304: }
305: return view;
306: }
307:
308: protected CompletionQuery createQuery() {
309: return null;
310: }
311:
312: public final CompletionQuery getQuery() {
313: if (query == null) {
314: query = createQuery();
315: }
316: return query;
317: }
318:
319: public JavaDocPane getJavaDocPane() {
320: if (javaDocPane == null) {
321: javaDocPane = new ScrollJavaDocPane(extEditorUI);
322: }
323: return javaDocPane;
324: }
325:
326: /**
327: * Get panel holding all aids (completion and documentation panes).
328: * @return JDCPopupPanel or <code>null</code>
329: */
330: public final JDCPopupPanel getJDCPopupPanelIfExists() {
331: return jdcPopupPanel;
332: }
333:
334: /**
335: * Get panel holding all aids (completion and documentation panes).
336: * @return JDCPopupPanel never <code>null</code>
337: */
338: public JDCPopupPanel getJDCPopupPanel() {
339: if (jdcPopupPanel == null) {
340: jdcPopupPanel = new JDCPopupPanel(extEditorUI,
341: getExtPane(), this );
342: }
343: return jdcPopupPanel;
344: }
345:
346: /** Get the result of the last valid completion query or null
347: * if there's no valid result available.
348: */
349: public synchronized final CompletionQuery.Result getLastResult() {
350: return lastResult;
351: }
352:
353: /** Reset the result of the last valid completion query. This
354: * is done for example after the document was modified.
355: */
356: public synchronized final void invalidateLastResult() {
357: currentTask().cancel();
358: lastResult = null;
359: caretPos = -1;
360: }
361:
362: private synchronized void setKeyPressed(boolean value) {
363: keyPressed = value;
364: }
365:
366: private synchronized boolean isKeyPressed() {
367: return keyPressed;
368: }
369:
370: public synchronized Object getSelectedValue() {
371: if (lastResult != null) {
372: int index = getView().getSelectedIndex();
373: if (index >= 0 && index < lastResult.getData().size()) {
374: return lastResult.getData().get(index);
375: }
376: }
377: return null;
378: }
379:
380: /** Return true if the completion should popup automatically */
381: public boolean isAutoPopupEnabled() {
382: return autoPopup;
383: }
384:
385: /** Return true when the pane exists and is visible.
386: * This is the preferred method of testing the visibility of the pane
387: * instead of <tt>getPane().isVisible()</tt> that forces
388: * the creation of the pane.
389: */
390: public boolean isPaneVisible() {
391: return (pane != null && pane.isVisible());
392: }
393:
394: /** Set the visibility of the view. This method should
395: * be used mainly for hiding the completion pane. If used
396: * with visible set to true it calls the <tt>popup(false)</tt>.
397: */
398: public void setPaneVisible(boolean visible) {
399: trace("ENTRY setPaneVisible " + visible); // NOI18N
400: if (visible) {
401: if (extEditorUI.getComponent() != null) {
402: popupImpl(false);
403: }
404: } else {
405: if (pane != null) {
406: cancelRequestImpl();
407: invalidateLastResult();
408: getJDCPopupPanel().setCompletionVisible(false);
409: caretPos = -1;
410: }
411: }
412: }
413:
414: public void completionCancel() {
415: trace("ENTRY completionCancel"); // NOI18N
416: if (pane != null) {
417: cancelRequestImpl();
418: invalidateLastResult();
419: caretPos = -1;
420: }
421: }
422:
423: /** Refresh the contents of the view if it's currently visible.
424: * @param postRequest post the request instead of refreshing the view
425: * immediately. The <tt>ExtSettingsNames.COMPLETION_REFRESH_DELAY</tt>
426: * setting stores the number of milliseconds before the view is refreshed.
427: */
428: public void refresh(boolean postRequest) {
429: trace("ENTRY refresh " + postRequest); // NOI18N
430: refreshImpl(postRequest);
431: }
432:
433: private synchronized void refreshImpl(final boolean postRequest) {
434:
435: // exit immediatelly
436: if (isPaneVisible() == false)
437: return;
438:
439: class RefreshTask implements Runnable {
440: private final boolean batch;
441:
442: RefreshTask(boolean batch) {
443: this .batch = batch;
444: }
445:
446: public void run() {
447: if (isPaneVisible()) {
448: timer.stop();
449: if (batch) {
450: timer.setInitialDelay(refreshDelay);
451: timer.setDelay(refreshDelay);
452: timer.start();
453: } else {
454: actionPerformed(null);
455: }
456: }
457: }
458: }
459: ;
460:
461: SwingUtilities.invokeLater(new RefreshTask(postRequest));
462: }
463:
464: /** Get the help and show it in the view. If the view is already visible
465: * perform the refresh of the view.
466: * @param postRequest post the request instead of displaying the view
467: * immediately. The <tt>ExtSettingsNames.COMPLETION_AUTO_POPUP_DELAY</tt>
468: * setting stores the number of milliseconds before the view is displayed.
469: * If the user presses a key until the delay expires nothing is shown.
470: * This guarantees that the user which knows what to write will not be
471: * annoyed with the unnecessary help.
472: */
473: public void popup(boolean postRequest) {
474: trace("ENTRY popup " + postRequest); // NOI18N
475: popupImpl(postRequest);
476: }
477:
478: private synchronized void popupImpl(boolean postRequest) {
479: if (isPaneVisible()) {
480: refreshImpl(postRequest);
481: } else {
482: timer.stop();
483: if (postRequest) {
484: timer.setInitialDelay(autoPopupDelay);
485: timer.setDelay(autoPopupDelay);
486: timer.start();
487: } else {
488: actionPerformed(null);
489: }
490: }
491: }
492:
493: /** Cancel last request for either displaying or refreshing
494: * the pane. It resets the internal timer.
495: */
496: public void cancelRequest() {
497: trace("ENTRY cancelRequest"); // NOI18N
498: cancelRequestImpl();
499: }
500:
501: private synchronized void cancelRequestImpl() {
502: timer.stop();
503: }
504:
505: /** Called to do either displaying or refreshing of the view.
506: * This method can be called either directly or because of the timer has fired.
507: * @param evt event describing the timer firing or null
508: * if the method was called directly because of the synchronous
509: * showing/refreshing the view.
510: */
511: public synchronized void actionPerformed(ActionEvent evt) {
512:
513: if (jdcPopupPanel == null)
514: extEditorUI.getCompletionJavaDoc(); //init javaDoc
515:
516: final JTextComponent component = extEditorUI.getComponent();
517: BaseDocument doc = Utilities.getDocument(component);
518:
519: if (component != null && doc != null) {
520:
521: provokedByAutoPopup = evt != null;
522:
523: try {
524: if ((caretPos != -1)
525: && (Utilities.getRowStart(component, component
526: .getCaret().getDot()) != Utilities
527: .getRowStart(component, caretPos))
528: && ((component.getCaret().getDot() - caretPos) > 0)) {
529: getJDCPopupPanel().setCompletionVisible(false);
530: caretPos = -1;
531: return;
532: }
533: } catch (BadLocationException ble) {
534: }
535:
536: caretPos = component.getCaret().getDot();
537:
538: // show progress view
539: class PendingTask extends CancelableRunnable {
540: public void run() {
541: if (cancelled())
542: return;
543: SwingUtilities.invokeLater(new Runnable() {
544: public void run() {
545: if (cancelled())
546: return;
547: performWait();
548: }
549: });
550: }
551: }
552: ;
553:
554: // perform query and show results
555: class QueryTask extends CancelableRunnable {
556: private final CancelableRunnable wait;
557: private final boolean isPaneVisible;
558:
559: public QueryTask(CancelableRunnable wait,
560: boolean isPaneVisible) {
561: this .wait = wait;
562: this .isPaneVisible = isPaneVisible;
563: }
564:
565: public void run() {
566: if (cancelled())
567: return;
568: try {
569: performQuery(component);
570: } catch (ThreadDeath td) {
571: throw td;
572: } catch (Throwable exc) {
573: exc.printStackTrace();
574: } finally {
575: wait.cancel();
576: if (cancelled())
577: return;
578: SwingUtilities.invokeLater(new Runnable() {
579: public void run() {
580: if (cancelled())
581: return;
582: CompletionQuery.Result res = lastResult;
583: if (res != null) {
584: if (instantSubstitution
585: && res.getData().size() == 1
586: && !isPaneVisible
587: && instantSubstitution(caretPos)) {
588: setPaneVisible(false);
589: return;
590: }
591: }
592:
593: performResults();
594: }
595: });
596: }
597: }
598:
599: void cancel() {
600: super .cancel();
601: wait.cancel();
602: }
603: }
604: ;
605:
606: // update current task: cancel pending task and fire new one
607:
608: currentTask().cancel();
609:
610: RequestProcessor rp;
611: boolean reentrantProvider = getQuery() instanceof CompletionQuery.SupportsSpeculativeInvocation;
612: if (reentrantProvider) {
613: rp = RequestProcessor.getDefault();
614: } else {
615: rp = getSerialiazingRequestProcessor();
616: }
617:
618: CancelableRunnable wait = new PendingTask();
619: CancelableRunnable task = new QueryTask(wait, getPane()
620: .isVisible());
621: currentTask(task);
622: if (provokedByAutoPopup == false) {
623: RequestProcessor.getDefault().post(wait, 100);
624: }
625: rp.post(task);
626: }
627: }
628:
629: /**
630: * Show wait completion result. Always called from AWT.
631: */
632: private void performWait() {
633: getPane().setTitle(
634: NbBundle.getBundle(org.netbeans.editor.BaseKit.class)
635: .getString("ext.Completion.wait"));
636: getView().setResult((CompletionQuery.Result) null);
637: if (isPaneVisible()) {
638: getJDCPopupPanel().refresh();
639: } else {
640: getJDCPopupPanel().setCompletionVisible(true);
641: }
642: }
643:
644: /**
645: * Execute complegtion query subtask
646: */
647: private void performQuery(final JTextComponent target) {
648:
649: BaseDocument doc = Utilities.getDocument(target);
650: long start = System.currentTimeMillis();
651: try {
652: lastResult = getQuery().query(target, caretPos,
653: doc.getSyntaxSupport());
654: } finally {
655: trace("performQuery took "
656: + (System.currentTimeMillis() - start) + "ms"); // NOI18N
657: setKeyPressed(false);
658: }
659: }
660:
661: /**
662: * Show result popup. Always called from AWT.
663: */
664: protected void performResults() {
665: // sample
666: CompletionQuery.Result res = lastResult;
667: if (res != null) {
668:
669: if (instantSubstitution && res.getData().size() == 1
670: && !isPaneVisible()
671: && instantSubstitution(caretPos))
672: return;
673:
674: getPane().setTitle(res.getTitle());
675: getView().setResult(res);
676: if (isPaneVisible()) {
677: getJDCPopupPanel().refresh();
678: } else {
679: getJDCPopupPanel().setCompletionVisible(true);
680: }
681: } else {
682: getJDCPopupPanel().setCompletionVisible(false);
683:
684: if (!isKeyPressed()) {
685: caretPos = -1;
686: } else {
687: setKeyPressed(false);
688: }
689: }
690: }
691:
692: /** Performs instant text substitution, provided that result contains only one
693: * item and completion has been invoked at the end of the word.
694: * @param caretPos offset position of the caret
695: */
696: public boolean instantSubstitution(int caretPos) {
697: trace("ENTRY instantSubstitution " + caretPos); // NOI18N
698: return instantSubstitutionImpl(caretPos);
699: }
700:
701: private synchronized boolean instantSubstitutionImpl(int caretPos) {
702: if (getLastResult() == null)
703: return false;
704: JTextComponent comp = extEditorUI.getComponent();
705: try {
706: if (comp != null) {
707: int[] block = Utilities.getIdentifierBlock(comp,
708: caretPos);
709: if (block == null || block[1] == caretPos)
710: return getLastResult().substituteText(0, false);
711: }
712: } catch (BadLocationException ble) {
713: }
714: return false;
715: }
716:
717: /** Substitute the document's text with the text
718: * that is appopriate for the selection
719: * in the view. This function is usually triggered
720: * upon pressing the Enter key.
721: * @return true if the substitution was performed
722: * false if not.
723: */
724: public synchronized boolean substituteText(boolean shift) {
725: trace("ENTRY substituteText " + shift); // NOI18N
726: if (lastResult != null) {
727: int index = getView().getSelectedIndex();
728: if (index >= 0) {
729: lastResult.substituteText(index, shift);
730: }
731: return true;
732: } else {
733: return false;
734: }
735: }
736:
737: public synchronized boolean substituteSimpleText() {
738: return false;
739: }
740:
741: /** Substitute the text with the longest common
742: * part of all the entries appearing in the view.
743: * This function is usually triggered
744: * upon pressing the Tab key.
745: * @return true if the substitution was performed
746: * false if not.
747: */
748: public synchronized boolean substituteCommonText() {
749: trace("ENTRY substituteCommonText"); // NOI18N
750: if (lastResult != null) {
751: int index = getView().getSelectedIndex();
752: if (index >= 0) {
753: lastResult.substituteCommonText(index);
754: }
755: return true;
756: } else {
757: return false;
758: }
759: }
760:
761: // Task management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
762:
763: /**
764: * Make given task current.
765: */
766: private void currentTask(CancelableRunnable task) {
767: cancellable = task;
768: }
769:
770: /**
771: * Get current task
772: */
773: private CancelableRunnable currentTask() {
774: return cancellable;
775: }
776:
777: /**
778: * Multistage task can test its cancel status after every atomic
779: * (non-cancellable) stage.
780: */
781: abstract class CancelableRunnable implements Runnable {
782: private boolean cancelled = false;
783:
784: boolean cancelled() {
785: return cancelled;
786: }
787:
788: void cancel() {
789: cancelled = true;
790: }
791: }
792:
793: /**
794: * Get serializing request processor.
795: */
796: private synchronized RequestProcessor getSerialiazingRequestProcessor() {
797: if (serializingRequestProcessor == null) {
798: serializingRequestProcessor = new RequestProcessor(
799: "editor.completion", 1);// NOI18N
800: }
801: return serializingRequestProcessor;
802: }
803:
804: // Debug support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
805:
806: private static void trace(String msg) {
807: if (DEBUG_COMPLETION
808: && Boolean.getBoolean(PROP_DEBUG_COMPLETION)) {
809: synchronized (System.err) {
810: System.err.println(msg);
811: }
812: }
813: }
814: }
|