/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.das2.jythoncompletion.ui; import org.das2.jythoncompletion.nbadapt.Utilities; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import javax.swing.*; import javax.swing.event.ListSelectionListener; import javax.swing.event.CaretListener; import javax.swing.event.DocumentListener; import javax.swing.plaf.TextUI; import javax.swing.text.*; import org.das2.jythoncompletion.JythonCompletionProvider; import org.das2.jythoncompletion.nbadapt.BaseDocument; import org.das2.jythoncompletion.nbadapt.GuardedDocument; import org.das2.jythoncompletion.support.CompletionItem; import org.das2.jythoncompletion.support.CompletionProvider; import org.das2.jythoncompletion.support.CompletionResultSet; import org.das2.jythoncompletion.support.CompletionTask; import org.das2.jythoncompletion.support.LazyCompletionItem; /** * Implementation of the completion processing. * The visual related processing is done in AWT thread together * with completion providers invocation and result set sorting. *
* The only thing that can be done outside of the AWT * is hiding of the completion/documentation/tooltip. * *

* The completion providers typically reschedule computation intensive * collecting of their result set into an extra thread to keep the GUI responsive. * * @author Dusan Balek, Miloslav Metelka */ public class CompletionImpl extends MouseAdapter implements DocumentListener, CaretListener, KeyListener, FocusListener, ListSelectionListener, PropertyChangeListener { private static final Logger logger= Logger.getLogger("jython.editor"); private static final boolean debug = Boolean.getBoolean("org.netbeans.modules.editor.completion.debug"); private static final boolean alphaSort = Boolean.getBoolean("org.netbeans.modules.editor.completion.alphabeticalSort"); // [TODO] create an option private static final Logger UI_LOG = Logger.getLogger("org.netbeans.ui.editor.completion"); // NOI18N private static CompletionImpl singleton = null; private static final String NO_SUGGESTIONS = "no suggestions"; private static final String PLEASE_WAIT = "please wait"; private static final String COMPLETION_SHOW = "completion-show"; //NOI18N private static final String COMPLETION_ALL_SHOW = "completion-all-show"; //NOI18N private static final String DOC_SHOW = "doc-show"; //NOI18N private static final String TOOLTIP_SHOW = "tooltip-show"; //NOI18N private static final int PLEASE_WAIT_TIMEOUT = 750; private static final int PRESCAN = 25; public static CompletionImpl get() { if (singleton == null) singleton = new CompletionImpl(); return singleton; } static LazyListModel.Filter filter = new LazyListModel.Filter() { public boolean accept(Object obj) { if (obj instanceof LazyCompletionItem) return ((LazyCompletionItem)obj).accept(); return true; } public void scheduleUpdate(Runnable run) { SwingUtilities.invokeLater( run ); } }; /** Text component being currently edited. Changed in AWT only. */ private WeakReference activeComponent = null; /** Document currently installed in the active component. Changed in AWT only. */ private WeakReference activeDocument = null; /** Map containing keystrokes that should be overriden by completion processing. Changed in AWT only. */ private InputMap inputMap; /** Action map containing actions bound to keys through input map. Changed in AWT only. */ private ActionMap actionMap; /** Layout of the completion pane/documentation/tooltip. Changed in AWT only. */ private final CompletionLayout layout = new CompletionLayout(); /* Completion providers registered for the active component (its mime-type). Changed in AWT only. */ private CompletionProvider[] activeProviders = null; /** Mapping of mime-type to array of providers. Changed in AWT only. */ private HashMap providersCache = new HashMap(); /** * Result of the completion query. *
* It may be null which means that the query was cancelled. *
* Initiated in AWT and can be cleared from the thread that cancels the completion query. */ private Result completionResult; /** * Result of the documentation query. *
* It may be null which means that the query was cancelled. *
* Initiated in AWT and can be cleared from the thread that cancels the documentation query. */ private Result docResult; /** * Result of the tooltip query. *
* It may be null which means that the query was cancelled. *
* Initiated in AWT and can be cleared from the thread that cancels the tooltip query. */ private Result toolTipResult; /** Timer for opening completion automatically. Changed in AWT only. */ private Timer completionAutoPopupTimer; /** Timer for opening documentation window automatically. Changed in AWT only. */ private Timer docAutoPopupTimer; /** Timer for opening Please Wait popup. Changed in AWT only. */ private Timer pleaseWaitTimer; /** Whether it's initial or refreshed query. Changed in AWT only. */ private boolean refreshedQuery = false; /** Whether it's explicit or automatic query. Changed in AWT only. */ private boolean explicitQuery = false; private WeakReference lastSelectedItem = null; /** Ending offset of the recent autopopup modification. */ private int autoModEndOffset; private boolean pleaseWaitDisplayed = false; private String completionShortcut = null; private CompletionImpl() { /*EditorRegistry.addPropertyChangeListener(this);*/ completionAutoPopupTimer = new Timer(0, new ActionListener() { public void actionPerformed(ActionEvent e) { Result localCompletionResult; synchronized (this) { localCompletionResult = completionResult; } if (localCompletionResult != null && !localCompletionResult.isQueryInvoked()) { pleaseWaitTimer.restart(); queryResultSets(localCompletionResult.getResultSets()); localCompletionResult.queryInvoked(); } } }); completionAutoPopupTimer.setRepeats(false); docAutoPopupTimer = new Timer(0, new ActionListener() { public void actionPerformed(ActionEvent e) { if (lastSelectedItem == null || lastSelectedItem.get() != layout.getSelectedCompletionItem()) showDocumentation(); } }); docAutoPopupTimer.setRepeats(false); pleaseWaitTimer = new Timer(PLEASE_WAIT_TIMEOUT, new ActionListener() { public void actionPerformed(ActionEvent e) { String waitText = PLEASE_WAIT; Result localCompletionResult; synchronized (this) { localCompletionResult = completionResult; } List resultSets; if (localCompletionResult != null && (resultSets = localCompletionResult.getResultSets()) != null) { for (Iterator it = resultSets.iterator(); it.hasNext();) { CompletionResultSetImpl resultSet = (CompletionResultSetImpl)it.next(); if (resultSet != null && resultSet.getWaitText() != null) { waitText = resultSet.getWaitText(); break; } } } layout.showCompletion(Collections.singletonList(waitText), null, -1, CompletionImpl.this, null, 0); pleaseWaitDisplayed = true; } }); pleaseWaitTimer.setRepeats(false); //Settings.addSettingsChangeListener(this); } private JTextComponent getActiveComponent() { return activeComponent != null ? activeComponent.get() : null; } private Document getActiveDocument() { return activeDocument != null ? activeDocument.get() : null; } int getSortType() { return alphaSort ? CompletionResultSet.TEXT_SORT_TYPE : CompletionResultSet.PRIORITY_SORT_TYPE; } public void insertUpdate(javax.swing.event.DocumentEvent e) { // Ignore insertions done outside of the AWT (various content generation) if (!SwingUtilities.isEventDispatchThread()) { return; } // Check whether the insertion came from typing //if (!DocumentUtilities.isTypingModification(e)) { // return; //} if (activeProviders != null) { try { int modEndOffset = e.getOffset() + e.getLength(); if (getActiveComponent().getSelectionStart() != modEndOffset) return; String typedText = e.getDocument().getText(e.getOffset(), e.getLength()); for (int i = 0; i < activeProviders.length; i++) { int type = activeProviders[i].getAutoQueryTypes(getActiveComponent(), typedText); boolean completionResultNull; synchronized (this) { completionResultNull = (completionResult == null); } if ((type & CompletionProvider.COMPLETION_QUERY_TYPE) != 0 && CompletionSettings.INSTANCE.completionAutoPopup()) { synchronized ( this ) { autoModEndOffset = modEndOffset; } if (completionResultNull) showCompletion(false, true, CompletionProvider.COMPLETION_QUERY_TYPE); } boolean tooltipResultNull; synchronized (this) { tooltipResultNull = (toolTipResult == null); } if (tooltipResultNull && (type & CompletionProvider.TOOLTIP_QUERY_TYPE) != 0) { showToolTip(); } } } catch (BadLocationException ex) {} if (completionAutoPopupTimer.isRunning()) restartCompletionAutoPopupTimer(); } } public void removeUpdate(javax.swing.event.DocumentEvent e) { // Ignore insertions done outside of the AWT (various content generation) if (!SwingUtilities.isEventDispatchThread()) { return; } } public void changedUpdate(javax.swing.event.DocumentEvent e) { } public synchronized void caretUpdate(javax.swing.event.CaretEvent e) { assert (SwingUtilities.isEventDispatchThread()); if (activeProviders != null) { // Check whether there is an active result being computed but not yet displayed // Caret update should be notified AFTER document modifications // thank to document listener priorities Result localCompletionResult; localCompletionResult = completionResult; if ((completionAutoPopupTimer.isRunning() || localCompletionResult != null) && (!layout.isCompletionVisible() || pleaseWaitDisplayed) && e.getDot() != autoModEndOffset) { hideCompletion(false); } SwingUtilities.invokeLater(new Runnable() { public void run() { completionRefresh(); toolTipRefresh(); } }); } } public void keyPressed(KeyEvent e) { dispatchKeyEvent(e); } public void keyReleased(KeyEvent e) { dispatchKeyEvent(e); } public void keyTyped(KeyEvent e) { dispatchKeyEvent(e); } public void focusGained(FocusEvent e) { } public void focusLost(FocusEvent e) { hideAll(); } public void mouseClicked(MouseEvent e) { hideAll(); } public void hideAll() { hideToolTip(); hideCompletion(true); hideDocumentation(true); } /** * Called from AWT when selection in the completion list pane changes. */ public void valueChanged(javax.swing.event.ListSelectionEvent e) { assert (SwingUtilities.isEventDispatchThread()); if (layout.isDocumentationVisible() || CompletionSettings.INSTANCE.documentationAutoPopup()) { restartDocumentationAutoPopupTimer(); } } /** * Expected to be called from the AWT only. */ public void propertyChange(PropertyChangeEvent e) { //assert (SwingUtilities.isEventDispatchThread()); // expected in AWT only //JTextComponent component = EditorRegistry.lastFocusedComponent(); //startPopup(component); } private void restartCompletionAutoPopupTimer() { assert (SwingUtilities.isEventDispatchThread()); // expect in AWT only int completionDelay = CompletionSettings.INSTANCE.completionAutoPopupDelay(); completionAutoPopupTimer.setInitialDelay(completionDelay); completionAutoPopupTimer.restart(); } private void restartDocumentationAutoPopupTimer() { assert (SwingUtilities.isEventDispatchThread()); // expect in AWT only int docDelay = CompletionSettings.INSTANCE.documentationAutoPopupDelay(); docAutoPopupTimer.setInitialDelay(docDelay); docAutoPopupTimer.restart(); } private CompletionProvider[] getCompletionProvidersForComponent(JTextComponent component) { assert (SwingUtilities.isEventDispatchThread()); if (component == null) return null; return new CompletionProvider[] { JythonCompletionProvider.getInstance() }; /* Object mimeTypeObj = component.getDocument().getProperty("mimeType"); //NOI18N String mimeType; if (mimeTypeObj instanceof String) mimeType = (String) mimeTypeObj; else { BaseKit kit = Utilities.getKit(component); if (kit == null) { return new CompletionProvider[0]; } mimeType = kit.getContentType(); } if (providersCache.containsKey(mimeType)) return providersCache.get(mimeType); Lookup lookup = MimeLookup.getLookup(MimePath.get(mimeType)); Collection col = lookup.lookupAll(CompletionProvider.class); int size = col.size(); CompletionProvider[] ret = size == 0 ? null : col.toArray(new CompletionProvider[size]); providersCache.put(mimeType, ret); return ret; */ } private void dispatchKeyEvent(KeyEvent e) { if (e == null) return; KeyStroke ks = KeyStroke.getKeyStrokeForEvent(e); JTextComponent comp = getActiveComponent(); boolean compEditable = (comp != null && comp.isEditable()); boolean guardedPos = false; //fcomp.getDocument() instanceof GuardedDocument && ((GuardedDocument)comp.getDocument()).isPosGuarded(comp.getSelectionEnd()); Object obj = inputMap.get(ks); if (obj != null) { Action action = actionMap.get(obj); if (action != null) { if (compEditable) action.actionPerformed(null); e.consume(); return; } } if (layout.isCompletionVisible()) { CompletionItem item = layout.getSelectedCompletionItem(); if (item != null) { if (compEditable && !guardedPos) { LogRecord r = new LogRecord(Level.FINE, "COMPL_KEY_SELECT"); // NOI18N r.setParameters(new Object[] {e.getKeyChar(), layout.getSelectedIndex(), item.getClass().getSimpleName()}); item.processKeyEvent(e); if (e.isConsumed()) { uilog(r); return; } } // Call default action if ENTER was pressed if (e.getKeyCode() == KeyEvent.VK_ENTER && e.getID() == KeyEvent.KEY_PRESSED) { e.consume(); if (guardedPos) { Toolkit.getDefaultToolkit().beep(); } else if (compEditable) { LogRecord r = new LogRecord(Level.FINE, "COMPL_KEY_SELECT_DEFAULT"); // NOI18N r.setParameters(new Object[] {'\n', layout.getSelectedIndex(), item.getClass().getSimpleName()}); item.defaultAction(getActiveComponent()); uilog(r); } return; } } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_PAGE_UP || e.getKeyCode() == KeyEvent.VK_PAGE_DOWN || e.getKeyCode() == KeyEvent.VK_HOME || e.getKeyCode() == KeyEvent.VK_END || e.getKeyCode() == KeyEvent.VK_ENTER ) { hideCompletion(false); } if ( tabIsCompletion && e.getKeyCode() == KeyEvent.VK_TAB) { e.consume(); if (guardedPos) { Toolkit.getDefaultToolkit().beep(); } else if (compEditable && e.getID() == KeyEvent.KEY_PRESSED) insertCommonPrefix(); return; } } layout.processKeyEvent(e); } private void completionQuery(boolean delayQuery, int queryType) { refreshedQuery = false; Result newCompletionResult = new Result(activeProviders.length); synchronized (this) { assert (completionResult == null); completionResult = newCompletionResult; } List completionResultSets = newCompletionResult.getResultSets(); // Initialize the completion tasks for (int i = 0; i < activeProviders.length; i++) { CompletionTask compTask = activeProviders[i].createTask( queryType, getActiveComponent()); if (compTask != null) { CompletionResultSetImpl resultSet = new CompletionResultSetImpl( this, newCompletionResult, compTask, queryType); completionResultSets.add(resultSet); } } if (completionResultSets.size() > 0) { // Query the tasks if (delayQuery) { restartCompletionAutoPopupTimer(); } else { pleaseWaitTimer.restart(); queryResultSets(completionResultSets); newCompletionResult.queryInvoked(); } } else { completionCancel(); layout.showCompletion(Collections.singletonList(NO_SUGGESTIONS), null, -1, CompletionImpl.this, null, 0); pleaseWaitDisplayed = false; } } /** * Called from caretUpdate() to refresh the completion result after caret move. *
* Must be called in AWT thread. */ private void completionRefresh() { Result localCompletionResult; synchronized (this) { localCompletionResult = completionResult; } if (localCompletionResult != null) { refreshedQuery = true; Result refreshResult = localCompletionResult.createRefreshResult(); synchronized (this) { completionResult = refreshResult; } refreshResult.invokeRefresh(); } } private void completionCancel() { Result oldCompletionResult; synchronized (this) { oldCompletionResult = completionResult; completionResult = null; } if (oldCompletionResult != null) { oldCompletionResult.cancel(); } } /** * Called from dispatchKeyEvent() to insert prefix common to all items in the * completion result after TAB.
* Must be called in AWT thread after all tasks of the current completionResult are finished. */ private void insertCommonPrefix() { JTextComponent c = getActiveComponent(); Result localCompletionResult; synchronized (this) { localCompletionResult = completionResult; if (localCompletionResult == null) return; if (!isAllResultsFinished(localCompletionResult.resultSets)) { Toolkit.getDefaultToolkit().beep(); return; } } CharSequence commonText = null; int anchorOffset = -1; outer: for (Iterator it = localCompletionResult.getResultSets().iterator(); it.hasNext();) { CompletionResultSetImpl resultSet = (CompletionResultSetImpl)it.next(); List resultItems = resultSet.getItems(); if (resultItems.size() > 0) { if (anchorOffset >= -1) { if (anchorOffset > -1 && anchorOffset != resultSet.getAnchorOffset()) anchorOffset = -2; else anchorOffset = resultSet.getAnchorOffset(); } for (Iterator itt = resultItems.iterator(); itt.hasNext();) { CharSequence text = ((CompletionItem)itt.next()).getInsertPrefix(); if (text == null) { commonText = null; break outer; } if (commonText == null) { commonText = text; } else { // Get the largest common part int minLen = Math.min(text.length(), commonText.length()); for (int commonInd = 0; commonInd < minLen; commonInd++) { if (text.charAt(commonInd) != commonText.charAt(commonInd)) { if (commonInd == 0) { commonText = null; break outer; // no common text } commonText = commonText.subSequence(0, commonInd); break; } } } } } } if (commonText != null && anchorOffset >= 0) { int caretOffset = c.getSelectionStart(); if (caretOffset - anchorOffset < commonText.length()) { Document doc = getActiveDocument(); BaseDocument baseDoc = null; if(doc instanceof BaseDocument) baseDoc = (BaseDocument)doc; // Insert the missing end part of the prefix if(baseDoc != null) baseDoc.atomicLock(); try { doc.remove(anchorOffset, caretOffset - anchorOffset); doc.insertString(anchorOffset, commonText.toString(), null); } catch (BadLocationException e) { } finally { if(baseDoc != null) baseDoc.atomicUnlock(); } } } } /** * May be called from any thread but it will be rescheduled into AWT. */ public void showCompletion() { showCompletion(true, false, CompletionProvider.COMPLETION_QUERY_TYPE); } private void showCompletion(boolean explicitQuery, boolean delayQuery, int queryType) { if (!SwingUtilities.isEventDispatchThread()) { // Re-call this method in AWT if necessary SwingUtilities.invokeLater(new ParamRunnable(ParamRunnable.SHOW_COMPLETION, explicitQuery, delayQuery, queryType)); return; } getActiveComponent().putClientProperty("completion-active", Boolean.TRUE); LogRecord r = new LogRecord(Level.FINE, "COMPL_INVOCATION"); // NOI18N r.setParameters(new Object[] {explicitQuery}); uilog(r); this.explicitQuery = explicitQuery; if (activeProviders != null) { completionAutoPopupTimer.stop(); synchronized(this) { if (explicitQuery && completionResult != null) { for (CompletionResultSetImpl rSet : completionResult.resultSets) { if (rSet.getQueryType() == CompletionProvider.COMPLETION_ALL_QUERY_TYPE) return; else break; } queryType = CompletionProvider.COMPLETION_ALL_QUERY_TYPE; } } completionCancel(); // cancel possibly pending query completionQuery(delayQuery, queryType); } } /** * Request displaying of the completion pane. * Can be called from any thread - is called synchronously * from the thread that finished last unfinished result. */ void requestShowCompletionPane(Result result) { pleaseWaitTimer.stop(); // Compute total count of the result sets int size = 0; int qType = 0; List completionResultSets = result.getResultSets(); for (int i = completionResultSets.size() - 1; i >= 0; i--) { CompletionResultSetImpl resultSet = completionResultSets.get(i); size += resultSet.getItems().size(); qType = resultSet.getQueryType(); } // Collect and sort the gathered completion items List resultItems = new ArrayList(size); String title = null; int anchorOffset = -1; boolean hasAdditionalItems = false; if (size > 0) { for (int i = 0; i < completionResultSets.size(); i++) { CompletionResultSetImpl resultSet = completionResultSets.get(i); List items = resultSet.getItems(); if (items.size() > 0) { resultItems.addAll(items); if (title == null) title = resultSet.getTitle(); if (!hasAdditionalItems) hasAdditionalItems = resultSet.hasAdditionalItems(); if (anchorOffset == -1) anchorOffset = resultSet.getAnchorOffset(); } } } final ArrayList sortedResultItems = new ArrayList(size = resultItems.size()); if (size > 0) { Collections.sort(resultItems, CompletionItemComparator.get(getSortType())); int cnt = 0; for(int i = 0; i < size; i++) { CompletionItem item = resultItems.get(i); if (cnt < PRESCAN ) { if (!filter.accept(item)) continue; else sortedResultItems.add( item ); } else { sortedResultItems.add(item); } cnt++; } } final boolean noSuggestions = sortedResultItems.size() == 0; if (noSuggestions && qType == CompletionProvider.COMPLETION_QUERY_TYPE) { showCompletion(this.explicitQuery, false, CompletionProvider.COMPLETION_ALL_QUERY_TYPE); return; } // Request displaying of the completion pane in AWT thread final String displayTitle = title; final int displayAnchorOffset = anchorOffset; final boolean displayAdditionalItems = hasAdditionalItems; Runnable requestShowRunnable = new Runnable() { public void run() { JTextComponent c = getActiveComponent(); int caretOffset = c.getSelectionStart(); // completionResults = null; if (sortedResultItems.size() == 1 && !refreshedQuery && explicitQuery && CompletionSettings.INSTANCE.completionInstantSubstitution() && c.isEditable() && !(c.getDocument() instanceof GuardedDocument && ((GuardedDocument)c.getDocument()).isPosGuarded(caretOffset))) { try { int[] block = Utilities.getIdentifierBlock(c, caretOffset); if (block == null || block[1] == caretOffset) { // NOI18N CompletionItem item = sortedResultItems.get(0); if (item.instantSubstitution(c)) { return; } } } catch (BadLocationException ex) { } } int selectedIndex = getCompletionPreSelectionIndex(sortedResultItems); getActiveComponent().putClientProperty("completion-visible", Boolean.TRUE); layout.showCompletion(noSuggestions ? Collections.singletonList(NO_SUGGESTIONS) : sortedResultItems, displayTitle, displayAnchorOffset, CompletionImpl.this, displayAdditionalItems ? completionShortcut : null, selectedIndex); pleaseWaitDisplayed = false; // Show documentation as well if set by default if (CompletionSettings.INSTANCE.documentationAutoPopup()) { if (noSuggestions) { docAutoPopupTimer.stop(); // Ensure the popup timer gets stopped documentationCancel(); layout.hideDocumentation(); } else { restartDocumentationAutoPopupTimer(); } } } }; runInAWT(requestShowRunnable); } private int getCompletionPreSelectionIndex(List items) { String prefix = null; if(getActiveDocument() instanceof BaseDocument) { BaseDocument doc = (BaseDocument)getActiveDocument(); int caretOffset = getActiveComponent().getSelectionStart(); try { int[] block = Utilities.getIdentifierBlock(doc, caretOffset); if (block != null) { block[1] = caretOffset; prefix = doc.getText( block[0], block[1] ); } } catch (BadLocationException ble) { } } if (prefix != null && prefix.length() > 0) { int idx = 0; for (CompletionItem item : items) { CharSequence text = item.getInsertPrefix(); if (text != null && text.toString().startsWith(prefix)) return idx; idx++; } } return 0; } /** * May be called from any thread. The UI changes will be rescheduled into AWT. */ public boolean hideCompletion() { return hideCompletion(true); } public boolean hideCompletion(boolean completionOnly) { completionCancel(); // Invoke hideCompletionPane() in AWT if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new ParamRunnable(ParamRunnable.HIDE_COMPLETION_PANE, completionOnly)); return false; } else { // in AWT return hideCompletionPane(completionOnly); } } /** * Hide the completion pane. This must be called in AWT thread. */ private boolean hideCompletionPane(boolean completionOnly) { completionAutoPopupTimer.stop(); // Ensure the popup timer gets stopped pleaseWaitTimer.stop(); boolean hidePerformed = layout.hideCompletion(); pleaseWaitDisplayed = false; if (!completionOnly && hidePerformed && CompletionSettings.INSTANCE.documentationAutoPopup()) { hideDocumentation(true); } getActiveComponent().putClientProperty("completion-visible", Boolean.FALSE); getActiveComponent().putClientProperty("completion-active", Boolean.FALSE); return hidePerformed; } /** * May be called from any thread but it will be rescheduled into AWT. */ public void showDocumentation() { if (!SwingUtilities.isEventDispatchThread()) { // Re-call this method in AWT if necessary SwingUtilities.invokeLater(new ParamRunnable(ParamRunnable.SHOW_DOCUMENTATION)); return; } if (activeProviders != null) { documentationCancel(); layout.clearDocumentationHistory(); documentationQuery(); } } /** * Request displaying of the documentation pane. * Can be called from any thread - is called synchronously * from the thread that finished last unfinished result. */ void requestShowDocumentationPane(Result result) { final CompletionResultSetImpl resultSet = findFirstValidResult(result.getResultSets()); runInAWT(new Runnable() { public void run() { synchronized (CompletionImpl.this) { if (resultSet != null) { layout.showDocumentation( resultSet.getDocumentation(), resultSet.getAnchorOffset()); } else { documentationCancel(); layout.hideDocumentation(); } } } }); } /** * May be called in AWT only. */ private void documentationQuery() { Result newDocumentationResult = new Result(1); // Estimate for selected item only List documentationResultSets; synchronized (this) { assert (docResult == null); docResult = newDocumentationResult; documentationResultSets= docResult.getResultSets(); } CompletionTask docTask; CompletionItem selectedItem = layout.getSelectedCompletionItem(); if (selectedItem != null) { lastSelectedItem = new WeakReference(selectedItem); docTask = selectedItem.createDocumentationTask(); if (docTask != null) { // attempt the documentation for selected item CompletionResultSetImpl resultSet = new CompletionResultSetImpl( this, newDocumentationResult, docTask, CompletionProvider.DOCUMENTATION_QUERY_TYPE); documentationResultSets.add(resultSet); } } else { // No item selected => Query all providers lastSelectedItem = null; for (int i = 0; i < activeProviders.length; i++) { docTask = activeProviders[i].createTask( CompletionProvider.DOCUMENTATION_QUERY_TYPE, getActiveComponent()); if (docTask != null) { CompletionResultSetImpl resultSet = new CompletionResultSetImpl( this, newDocumentationResult, docTask, CompletionProvider.DOCUMENTATION_QUERY_TYPE); documentationResultSets.add(resultSet); } } } if (documentationResultSets.size() > 0) { queryResultSets(documentationResultSets); newDocumentationResult.queryInvoked(); } else { documentationCancel(); layout.hideDocumentation(); } } private void documentationCancel() { Result oldDocumentationResult; synchronized (this) { oldDocumentationResult = docResult; docResult = null; } if (oldDocumentationResult != null) { oldDocumentationResult.cancel(); } } /** * May be called from any thread. The UI changes will be rescheduled into AWT. */ public boolean hideDocumentation() { return hideDocumentation(true); } boolean hideDocumentation(boolean documentationOnly) { documentationCancel(); // Invoke hideDocumentationPane() in AWT if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new ParamRunnable(ParamRunnable.HIDE_DOCUMENTATION_PANE, documentationOnly)); return false; } else { // in AWT return hideDocumentationPane(documentationOnly); } } /** * May be called in AWT only. */ boolean hideDocumentationPane(boolean documentationOnly) { // Ensure the documentation popup timer is stopped docAutoPopupTimer.stop(); boolean hidePerformed = layout.hideDocumentation(); // Also hide completion if documentation pops automatically if (!documentationOnly && hidePerformed && CompletionSettings.INSTANCE.documentationAutoPopup()) { hideCompletion(true); } return hidePerformed; } /** * May be called from any thread but it will be rescheduled into AWT. */ public void showToolTip() { if (!SwingUtilities.isEventDispatchThread()) { // Re-call this method in AWT if necessary SwingUtilities.invokeLater(new ParamRunnable(ParamRunnable.SHOW_TOOL_TIP)); return; } if (activeProviders != null) { toolTipCancel(); toolTipQuery(); } } /** * Request displaying of the tooltip pane. * Can be called from any thread - is called synchronously * from the thread that finished last unfinished result. */ void requestShowToolTipPane(Result result) { final CompletionResultSetImpl resultSet = findFirstValidResult(result.getResultSets()); runInAWT(new Runnable() { public void run() { if (resultSet != null) { layout.showToolTip( resultSet.getToolTip(), resultSet.getAnchorOffset()); } else { hideToolTip(); } } }); } public void startPopup(JTextComponent component) { boolean cancel = false; if (component != getActiveComponent()) { activeProviders = getCompletionProvidersForComponent(component); if (debug) { StringBuffer sb = new StringBuffer("Completion PROVIDERS:\n"); // NOI18N if (activeProviders != null) { for (int i = 0; i < activeProviders.length; i++) { sb.append("providers["); // NOI18N sb.append(i); sb.append("]: "); // NOI18N sb.append(activeProviders[i].getClass()); sb.append('\n'); } } logger.fine(sb.toString()); } if (getActiveComponent() != null) { getActiveComponent().removeCaretListener(this); getActiveComponent().removeKeyListener(this); getActiveComponent().removeFocusListener(this); getActiveComponent().removeMouseListener(this); } if (component != null) { if (activeProviders != null) { component.addCaretListener(this); component.addKeyListener(this); component.addFocusListener(this); component.addMouseListener(this); } } activeComponent = component != null ? new WeakReference(component) : null; CompletionSettings.INSTANCE.notifyEditorComponentChange(getActiveComponent()); layout.setEditorComponent(getActiveComponent()); installKeybindings(); cancel = true; } Document document = component==null ? null : component.getDocument(); if (component != null && document != getActiveDocument()) { activeProviders = getCompletionProvidersForComponent(component); if (debug) { StringBuffer sb = new StringBuffer("Completion PROVIDERS:\n"); // NOI18N if (activeProviders != null) { for (int i = 0; i < activeProviders.length; i++) { sb.append("providers["); // NOI18N sb.append(i); sb.append("]: "); // NOI18N sb.append(activeProviders[i].getClass()); sb.append('\n'); } } logger.fine(sb.toString()); } /* if (getActiveDocument() != null) { DocumentUtilities.removeDocumentListener(getActiveDocument(), this, DocumentListenerPriority.AFTER_CARET_UPDATE); } if (activeProviders != null) { DocumentUtilities.addDocumentListener(document, this, DocumentListenerPriority.AFTER_CARET_UPDATE); }*/ activeDocument = new WeakReference(document); cancel = true; } if (cancel) { completionCancel(); } } /** * May be called in AWT only. */ private void toolTipQuery() { Result newToolTipResult = new Result(1); synchronized (this) { assert (toolTipResult == null); toolTipResult = newToolTipResult; } List toolTipResultSets = newToolTipResult.getResultSets(); CompletionTask toolTipTask; CompletionItem selectedItem = layout.getSelectedCompletionItem(); if (selectedItem != null && (toolTipTask = selectedItem.createToolTipTask()) != null) { CompletionResultSetImpl resultSet = new CompletionResultSetImpl( this, newToolTipResult, toolTipTask, CompletionProvider.TOOLTIP_QUERY_TYPE); toolTipResultSets.add(resultSet); } else { for (int i = 0; i < activeProviders.length; i++) { toolTipTask = activeProviders[i].createTask( CompletionProvider.TOOLTIP_QUERY_TYPE, getActiveComponent()); if (toolTipTask != null) { CompletionResultSetImpl resultSet = new CompletionResultSetImpl( this, newToolTipResult, toolTipTask, CompletionProvider.TOOLTIP_QUERY_TYPE); toolTipResultSets.add(resultSet); } } } queryResultSets(toolTipResultSets); newToolTipResult.queryInvoked(); } private void toolTipRefresh() { Result localToolTipResult; synchronized (this) { localToolTipResult = toolTipResult; } if (localToolTipResult != null) { Result refreshResult = localToolTipResult.createRefreshResult(); synchronized (this) { toolTipResult = refreshResult; } refreshResult.invokeRefresh(); } } /** * May be called from any thread. */ private void toolTipCancel() { Result oldToolTipResult; synchronized (this) { oldToolTipResult = toolTipResult; toolTipResult = null; } if (oldToolTipResult != null) { oldToolTipResult.cancel(); } } /** * May be called from any thread. The UI changes will be rescheduled into AWT. */ public boolean hideToolTip() { toolTipCancel(); // Invoke hideToolTipPane() in AWT if (!SwingUtilities.isEventDispatchThread()) { SwingUtilities.invokeLater(new ParamRunnable(ParamRunnable.HIDE_TOOL_TIP_PANE)); return false; } else { // in AWT return hideToolTipPane(); } } /** * May be called in AWT only. */ boolean hideToolTipPane() { return layout.hideToolTip(); } /** Attempt to find the editor keystroke for the given editor action. */ private KeyStroke[] findEditorKeys(String editorActionName, KeyStroke ... defaultKey) { // This method is implemented due to the issue // #25715 - Attempt to search keymap for the keybinding that logically corresponds to the action KeyStroke[] ret = defaultKey; if (editorActionName != null && getActiveComponent() != null) { TextUI ui = getActiveComponent().getUI(); Keymap km = getActiveComponent().getKeymap(); if (ui != null && km != null) { /*EditorKit kit = ui.getEditorKit(getActiveComponent()); if (kit instanceof BaseKit) { Action a = ((BaseKit)kit).getActionByName(editorActionName); if (a != null) { KeyStroke[] keys = km.getKeyStrokesForAction(a); if (keys != null && keys.length > 0) { ret = keys; } else { // try kit's keymap Keymap km2 = ((BaseKit)kit).getKeymap(); KeyStroke[] keys2 = km2.getKeyStrokesForAction(a); if (keys2 != null && keys2.length > 0) { ret = keys2; } } } }*/ } } return ret; } private void installKeybindings() { actionMap = new ActionMap(); inputMap = new InputMap(); completionShortcut = null; KeyStroke[] keys; if ( JythonCompletionProvider.getInstance().settings().isTabIsCompletion() ) { // Register completion show keys= findEditorKeys( "completion", KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.CTRL_MASK), KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0 ) ); } else { // Register completion show keys= findEditorKeys( "completion", KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.CTRL_MASK) ); } for (int i = 0; i < keys.length; i++) { inputMap.put(keys[i], COMPLETION_SHOW); if (completionShortcut == null) { completionShortcut = getKeyStrokeAsText(keys[i]); } } if (completionShortcut == null) completionShortcut = "\'Ctrl+SPACE\'"; actionMap.put(COMPLETION_SHOW, new CompletionShowAction(CompletionProvider.COMPLETION_QUERY_TYPE)); } private static String getKeyStrokeAsText (KeyStroke keyStroke) { int modifiers = keyStroke.getModifiers (); StringBuffer sb = new StringBuffer (); sb.append('\''); if ((modifiers & InputEvent.CTRL_DOWN_MASK) > 0) sb.append ("Ctrl+"); //NOI18N if ((modifiers & InputEvent.ALT_DOWN_MASK) > 0) sb.append ("Alt+"); //NOI18N if ((modifiers & InputEvent.SHIFT_DOWN_MASK) > 0) sb.append ("Shift+"); //NOI18N if ((modifiers & InputEvent.META_DOWN_MASK) > 0) sb.append ("Meta+"); //NOI18N if (keyStroke.getKeyCode () != KeyEvent.VK_SHIFT && keyStroke.getKeyCode () != KeyEvent.VK_CONTROL && keyStroke.getKeyCode () != KeyEvent.VK_META && keyStroke.getKeyCode () != KeyEvent.VK_ALT && keyStroke.getKeyCode () != KeyEvent.VK_ALT_GRAPH ) sb.append ( "" + KeyStroke.getKeyStroke (keyStroke.getKeyCode (), 0 ) ); sb.append('\''); return sb.toString (); } /** * Notify that a particular completion result set has just been finished. *
* This method may be called from any thread. */ void finishNotify(CompletionResultSetImpl finishedResult) { Result localResult; boolean finished = false; switch (finishedResult.getQueryType()) { case CompletionProvider.COMPLETION_QUERY_TYPE: case CompletionProvider.COMPLETION_ALL_QUERY_TYPE: synchronized (this) { localResult = completionResult; if (finishedResult.getResultId() == localResult) { finished = isAllResultsFinished(localResult.getResultSets()); } } if (finished) requestShowCompletionPane(localResult); break; case CompletionProvider.DOCUMENTATION_QUERY_TYPE: synchronized (this) { localResult = docResult; if (finishedResult.getResultId() == localResult) { finished = isAllResultsFinished(localResult.getResultSets()); } } if (finished) requestShowDocumentationPane(localResult); break; case CompletionProvider.TOOLTIP_QUERY_TYPE: synchronized (this) { localResult = toolTipResult; if (finishedResult.getResultId() == localResult) { finished = isAllResultsFinished(localResult.getResultSets()); } } if (finished) requestShowToolTipPane(localResult); break; default: throw new IllegalStateException(); // Invalid query type } } private static boolean isAllResultsFinished(List resultSets) { for (int i = resultSets.size() - 1; i >= 0; i--) { CompletionResultSetImpl result = resultSets.get(i); if (!result.isFinished()) { if (debug) { logger.fine("CompletionTask: " + result.getTask() // NOI18N + " not finished yet"); // NOI18N } return false; } } if (debug) { logger.fine("----- All tasks finished -----"); } return true; } /** * Find first result that has non-null documentation or tooltip * depending on its query type. *
* The method assumes that all the resultSets are already finished. */ private static CompletionResultSetImpl findFirstValidResult(List resultSets) { for (int i = 0; i < resultSets.size(); i++) { CompletionResultSetImpl result = resultSets.get(i); switch (result.getQueryType()) { case CompletionProvider.DOCUMENTATION_QUERY_TYPE: if (result.getDocumentation() != null) { return result; } break; case CompletionProvider.TOOLTIP_QUERY_TYPE: if (result.getToolTip() != null) { return result; } break; default: throw new IllegalStateException(); } } return null; } private static void runInAWT(Runnable r) { if (SwingUtilities.isEventDispatchThread()) { r.run(); } else { SwingUtilities.invokeLater(r); } } // .......................................................................... CompletionLayout testGetCompletionLayout() { return layout; } void testSetActiveComponent(JTextComponent component) { activeComponent = new WeakReference(component); } boolean tabIsCompletion= true; public void setTabIsCompletion(boolean b) { this.tabIsCompletion= b; if ( !b ) { inputMap.remove( KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0 ) ); } else { inputMap.put( KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0 ), COMPLETION_SHOW ); } } // .......................................................................... private final class CompletionShowAction extends AbstractAction { private int queryType; private CompletionShowAction(int queryType) { this.queryType = queryType; } public void actionPerformed(ActionEvent e) { showCompletion(true, false, queryType); } } private final class DocShowAction extends AbstractAction { public void actionPerformed(ActionEvent e) { showDocumentation(); } } private final class ToolTipShowAction extends AbstractAction { public void actionPerformed(ActionEvent e) { showToolTip(); } } private final class ParamRunnable implements Runnable { private static final int SHOW_COMPLETION = 0; private static final int SHOW_DOCUMENTATION = 1; private static final int SHOW_TOOL_TIP = 2; private static final int HIDE_COMPLETION_PANE = 3; private static final int HIDE_DOCUMENTATION_PANE = 4; private static final int HIDE_TOOL_TIP_PANE = 5; private final int opCode; private final boolean explicit; private final boolean delayQuery; private final int type; ParamRunnable(int opCode) { this(opCode, false); } ParamRunnable(int opCode, boolean explicit) { this(opCode, explicit, false, CompletionProvider.COMPLETION_QUERY_TYPE); } ParamRunnable(int opCode, boolean explicit, boolean delayQuery, int type) { this.opCode = opCode; this.explicit = explicit; this.delayQuery = delayQuery; this.type = type; } public void run() { switch (opCode) { case SHOW_COMPLETION: showCompletion(explicitQuery, delayQuery, type); break; case SHOW_DOCUMENTATION: showDocumentation(); break; case SHOW_TOOL_TIP: showToolTip(); break; case HIDE_COMPLETION_PANE: hideCompletionPane(explicit); break; case HIDE_DOCUMENTATION_PANE: hideDocumentationPane(explicit); break; case HIDE_TOOL_TIP_PANE: hideToolTipPane(); break; default: throw new IllegalStateException(); } } } private static void queryResultSets(List resultSets) { for (int i = 0; i < resultSets.size(); i++) { CompletionResultSetImpl resultSet = resultSets.get(i); resultSet.getTask().query(resultSet.getResultSet()); } } private static void createRefreshResultSets(List resultSets, Result refreshResult) { List refreshResultSets = refreshResult.getResultSets(); int size = resultSets.size(); // Create new resultSets for (int i = 0; i < size; i++) { CompletionResultSetImpl result = resultSets.get(i); result.markInactive(); result = new CompletionResultSetImpl(result.getCompletionImpl(), refreshResult, result.getTask(), result.getQueryType()); refreshResultSets.add(result); } } private static void refreshResultSets(List resultSets, boolean beforeQuery) { try { int size = resultSets.size(); for (int i = 0; i < size; i++) { CompletionResultSetImpl result = resultSets.get(i); result.getTask().refresh(beforeQuery ? null : result.getResultSet()); } } catch (Exception ex) { throw new RuntimeException(ex); } } private static void cancelResultSets(List resultSets) { int size = resultSets.size(); for (int i = 0; i < size; i++) { CompletionResultSetImpl result = resultSets.get(i); result.markInactive(); result.getTask().cancel(); } } /* public void settingsChange(org.netbeans.editor.SettingsChangeEvent evt) { if( evt == null) { return; } String settingName = evt.getSettingName(); if (SettingsNames.KEY_BINDING_LIST.equals(settingName) || settingName == null){ Utilities.runInEventDispatchThread(new Runnable(){ public void run(){ installKeybindings(); } }); } }*/ /** * Result holding list of completion result sets. *
* Initially the result is in unprepared state which allows the holding * thread to add the result sets and start the tasks. *
* If another thread calls cancel() it has no effect except setting a flag * that is returned from the prepared() method. *
* If the result is finished then cancelling physically cancels the result sets. */ static final class Result { private final List resultSets; private boolean invoked; private boolean cancelled; private boolean beforeQuery = true; Result(int resultSetsSize) { resultSets = new ArrayList(resultSetsSize); } /** * Get the contained resultSets. * * @return non-null resultSets. */ List getResultSets() { return resultSets; } /** * Cancel the resultSets. *
* If the result is not prepared a flag that the result * was cancelled is turned on (and later returned from prepared()). *
* Otherwise physical cancellation of the result sets is done. */ void cancel() { boolean fin; synchronized (this) { assert (!cancelled); fin = invoked; if (!invoked) { cancelled = true; } } if (fin) { // already invoked cancelResultSets(resultSets); } } synchronized boolean isQueryInvoked() { return invoked; } /** * Mark the queries were invoked on the tasks in the result sets. * @return true if the result was cancelled in the meantime. */ boolean queryInvoked() { boolean canc; synchronized (this) { assert (!invoked); invoked = true; canc = cancelled; beforeQuery = false; } if (canc) { cancelResultSets(resultSets); } return canc; } /** * and return the new result set * containing the refreshed results. */ Result createRefreshResult() { synchronized (this) { if (cancelled) { return null; } if (beforeQuery) { return this; } assert (invoked); // had to be invoked invoked = false; } Result refreshResult = new Result(getResultSets().size()); refreshResult.beforeQuery = beforeQuery; createRefreshResultSets(resultSets, refreshResult); return refreshResult; } /** * Invoke refreshing of the result sets. * This method should be invoked on the result set returned from * {@link #createRefreshResult()}. */ void invokeRefresh() { refreshResultSets(getResultSets(), beforeQuery); if (!beforeQuery) queryInvoked(); } } public CompletionResultSetImpl createTestResultSet(CompletionTask task, int queryType) { return new CompletionResultSetImpl(this, "TestResult", task, queryType); } static void uilog(LogRecord rec) { //rec.setResourceBundle(NbBundle.getBundle(CompletionImpl.class)); //rec.setResourceBundleName(CompletionImpl.class.getPackage().getName() + ".Bundle"); // NOI18N //rec.setLoggerName(UI_LOG.getName()); UI_LOG.log(rec); } }