/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle 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 com.tvl.modules.editor.completion; import com.tvl.spi.editor.completion.CompletionController; import com.tvl.spi.editor.completion.CompletionController.Selection; import com.tvl.spi.editor.completion.CompletionControllerProvider; import com.tvl.spi.editor.completion.CompletionItem; import com.tvl.spi.editor.completion.CompletionProvider; import com.tvl.spi.editor.completion.CompletionResultSet; import com.tvl.spi.editor.completion.CompletionTask; import com.tvl.spi.editor.completion.LazyCompletionItem; import java.awt.Color; import java.awt.Container; import java.awt.Font; import java.awt.Graphics; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; import java.lang.reflect.Array; 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.AbstractAction; import javax.swing.Action; import javax.swing.ActionMap; import javax.swing.InputMap; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.UndoableEditEvent; import javax.swing.event.UndoableEditListener; import javax.swing.plaf.TextUI; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.EditorKit; import javax.swing.text.JTextComponent; import javax.swing.text.Keymap; import javax.swing.undo.UndoableEdit; import org.netbeans.api.editor.EditorRegistry; import org.netbeans.api.editor.mimelookup.MimeLookup; import org.netbeans.api.editor.mimelookup.MimePath; import org.netbeans.api.editor.settings.KeyBindingSettings; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.BaseKit; import org.netbeans.editor.GuardedDocument; import org.netbeans.editor.Utilities; import org.netbeans.editor.ext.ExtKit; import org.netbeans.lib.editor.util.swing.DocumentListenerPriority; import org.netbeans.lib.editor.util.swing.DocumentUtilities; import org.openide.ErrorManager; import org.openide.text.CloneableEditorSupport; import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.LookupEvent; import org.openide.util.LookupListener; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; import org.openide.util.WeakListeners; /** * Implementation of the completion processing. * The visual related processing is done in AWT thread together * with completion providers invocation and result set sorting. * <br> * The only thing that can be done outside of the AWT * is hiding of the completion/documentation/tooltip. * * <p> * 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 */ @NbBundle.Messages({ "completion-no-suggestions=No suggestions" }) @SuppressWarnings("ClassWithMultipleLoggers") public class CompletionImpl extends MouseAdapter implements DocumentListener, CaretListener, KeyListener, FocusListener, ListSelectionListener, PropertyChangeListener, ChangeListener { // -J-Dorg.netbeans.modules.editor.completion.CompletionImpl.level=FINE private static final Logger LOG = Logger.getLogger(CompletionImpl.class.getName()); private static final boolean alphaSort = Boolean.getBoolean("org.netbeans.modules.editor.completion.alphabeticalSort"); // [TODO] create an option private static final Object CT_HANDLER_DOC_PROPERTY = "code-template-insert-handler"; // NOI18N 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 = Bundle.completion_no_suggestions(); private static final String PLEASE_WAIT = Bundle.completion_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; /** * A default {@link CompletionController} which is used for the "Please wait..." * and "No suggestions" completion results. */ private final CompletionController FALLBACK_COMPLETION_CONTROLLER = new CompletionController() { @Override public void sortItems(List<? extends CompletionItem> items, int sortType) { assert items.size() == 1; } @Override public Selection getSelection(List<? extends CompletionItem> items, List<? extends CompletionItem> declarationItems) { assert items.size() == 1; return Selection.DEFAULT; } @Override public void defaultAction(CompletionItem bestMatch, boolean isSelected) { if (isSelected) { bestMatch.defaultAction(getActiveComponent()); } } @Override public void processKeyEvent(KeyEvent evt, CompletionItem bestMatch, boolean isSelected) { bestMatch.processKeyEvent(evt); } @Override public void render(Graphics g, Font defaultFont, Color foregroundColor, Color backgroundColor, Color selectedForegroundColor, Color selectedBackgroundColor, int width, int height, CompletionItem item, boolean isBestMatch, boolean isSelected) { if (isBestMatch) { // Clear the background g.setColor(selectedBackgroundColor); g.fillRect(0, 0, width, height); g.setColor(selectedForegroundColor); item.render(g, defaultFont, selectedForegroundColor, selectedBackgroundColor, width, height, isBestMatch); } else { // Clear the background g.setColor(backgroundColor); g.fillRect(0, 0, width, height); g.setColor(foregroundColor); item.render(g, defaultFont, foregroundColor, backgroundColor, width, height, isBestMatch); } } @Override public boolean instantSubstitution(CompletionItem uniqueMatch) { return uniqueMatch.instantSubstitution(getActiveComponent()); } }; public static CompletionImpl get() { if (singleton == null) singleton = new CompletionImpl(); return singleton; } static LazyListModel.Filter<Object> filter = new LazyListModel.Filter<Object>() { @Override public boolean accept(Object obj) { if (obj instanceof LazyCompletionItem) return ((LazyCompletionItem)obj).accept(); return true; } @Override public void scheduleUpdate(Runnable run) { SwingUtilities.invokeLater( run ); } }; /** Text component being currently edited. Changed in AWT only. */ private WeakReference<JTextComponent> activeComponent = null; /** Document currently installed in the active component. Changed in AWT only. */ private WeakReference<Document> 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; /** Completion providers registered for the active component (its mime-type). Changed in AWT only. */ private CompletionControllerProvider[] activeControllerProviders = null; /** Mapping of mime-type to service provider type to array of providers. Changed in AWT only. */ private HashMap<String, HashMap<Class<?>, Object[]>> providersCache = new HashMap<>(); /** * Result of the completion query. * <br> * It may be null which means that the query was cancelled. * <br> * Initiated in AWT and can be cleared from the thread that cancels the completion query. */ private Result completionResult; /** * Result of the documentation query. * <br> * It may be null which means that the query was cancelled. * <br> * Initiated in AWT and can be cleared from the thread that cancels the documentation query. */ private Result docResult; /** * Result of the tooltip query. * <br> * It may be null which means that the query was cancelled. * <br> * 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<CompletionItem> lastSelectedItem = null; /** Ending offset of the recent autopopup modification. */ private int autoModEndOffset = -1; private boolean pleaseWaitDisplayed = false; private String completionShortcut = null; private Lookup.Result<KeyBindingSettings> kbs; private RequestProcessor.Task asyncWarmUpTask = null; private String asyncWarmUpMimeType = null; private static CompletionImplProfile profile; private final LookupListener shortcutsTracker = new LookupListener() { @Override public void resultChanged(LookupEvent ev) { Utilities.runInEventDispatchThread(new Runnable(){ @Override public void run(){ installKeybindings(); } }); } }; private Point lastViewPosition; // Visible view in JViewport @SuppressWarnings("LeakingThisInConstructor") private CompletionImpl() { EditorRegistry.addPropertyChangeListener(this); completionAutoPopupTimer = new Timer(0, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Result localCompletionResult; synchronized (CompletionImpl.this) { localCompletionResult = completionResult; } if (localCompletionResult != null && !localCompletionResult.isQueryInvoked()) { pleaseWaitTimer.restart(); CompletionImpl.this.refreshedQuery = false; getActiveComponent().putClientProperty("completion-active", Boolean.TRUE); //NOI18N queryResultSets(localCompletionResult.getResultSets()); localCompletionResult.queryInvoked(); } } }); completionAutoPopupTimer.setRepeats(false); docAutoPopupTimer = new Timer(0, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { SelectedCompletionItem selectedItem = layout.getSelectedCompletionItem(); CompletionItem currentSelectedItem = selectedItem != null ? selectedItem.getItem() : null; if (lastSelectedItem == null || lastSelectedItem.get() != currentSelectedItem) showDocumentation(); } }); docAutoPopupTimer.setRepeats(false); pleaseWaitTimer = new Timer(PLEASE_WAIT_TIMEOUT, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String waitText = PLEASE_WAIT; boolean politeWaitText = false; Result localCompletionResult; synchronized (CompletionImpl.this) { localCompletionResult = completionResult; } List<CompletionResultSetImpl> resultSets; if (localCompletionResult != null && (resultSets = localCompletionResult.getResultSets()) != null) { for (Iterator<CompletionResultSetImpl> it = resultSets.iterator(); it.hasNext();) { CompletionResultSetImpl resultSet = it.next(); if (resultSet != null && resultSet.getWaitText() != null) { waitText = resultSet.getWaitText(); politeWaitText = true; break; } } } layout.showCompletion(Collections.singletonList(waitText), Collections.<CompletionItem>emptyList(), null, -1, CompletionImpl.this, null, null, FALLBACK_COMPLETION_CONTROLLER, CompletionController.Selection.DEFAULT); pleaseWaitDisplayed = true; if (!politeWaitText) { long when = System.currentTimeMillis() - PLEASE_WAIT_TIMEOUT; initializeProfiling(when); } } }); pleaseWaitTimer.setRepeats(false); kbs = MimeLookup.getLookup(MimePath.EMPTY).lookupResult(KeyBindingSettings.class); kbs.addLookupListener(WeakListeners.create(LookupListener.class, shortcutsTracker, kbs)); } 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; } @Override public void insertUpdate(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.getDocument())) { return; } if (ensureActiveProviders()) { try { int modEndOffset = e.getOffset() + e.getLength(); 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.getInstance(getActiveComponent()).completionAutoPopup()) { autoModEndOffset = modEndOffset; if (completionResultNull) showCompletion(false, false, true, type & (CompletionProvider.COMPLETION_QUERY_MASK | CompletionProvider.USER_QUERY_MASK)); } boolean tooltipResultNull; synchronized (this) { tooltipResultNull = (toolTipResult == null); } if (tooltipResultNull && (type & CompletionProvider.TOOLTIP_QUERY_TYPE) != 0) { showToolTip(); } } } catch (BadLocationException ex) {} if (completionAutoPopupTimer.isRunning()) restartCompletionAutoPopupTimer(); } } @Override @SuppressWarnings("UnnecessaryReturnStatement") public void removeUpdate(DocumentEvent e) { // Ignore insertions done outside of the AWT (various content generation) if (!SwingUtilities.isEventDispatchThread()) { return; } } @Override public void changedUpdate(DocumentEvent e) { } @Override public synchronized void caretUpdate(CaretEvent e) { assert (SwingUtilities.isEventDispatchThread()); if (ensureActiveProviders()) { // 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 = completionResult; if (autoModEndOffset >= 0 && e.getDot() != autoModEndOffset && (completionAutoPopupTimer.isRunning() || localCompletionResult != null) && (!layout.isCompletionVisible() || pleaseWaitDisplayed) && CompletionSettings.getInstance(getActiveComponent()).completionAutoPopupDelay() > 0) { hideCompletion(false); } completionRefresh(); toolTipRefresh(); } } @Override public void keyPressed(KeyEvent e) { dispatchKeyEvent(e); } @Override public void keyReleased(KeyEvent e) { dispatchKeyEvent(e); } @Override public void keyTyped(KeyEvent e) { dispatchKeyEvent(e); } @Override public void focusGained(FocusEvent e) { } @Override public void focusLost(FocusEvent e) { hideAll(); } @Override public void mouseClicked(MouseEvent e) { hideAll(); } @Override public void stateChanged(ChangeEvent e) { // From JViewport boolean hide = true; JTextComponent component = getActiveComponent(); Container parent = component != null ? component.getParent() : null; if (parent instanceof JViewport) { JViewport viewport = (JViewport) parent; Point viewPosition = viewport.getViewPosition(); if (lastViewPosition != null && lastViewPosition.y == viewPosition.y) { hide = false; } lastViewPosition = viewPosition; } if (hide) { hideAll(); } } public void hideAll() { hideToolTip(); hideCompletion(true); hideDocumentation(true); } /** * Called from AWT when selection in the completion list pane changes. */ @Override public void valueChanged(ListSelectionEvent e) { assert (SwingUtilities.isEventDispatchThread()); documentationCancel(); if (layout.isDocumentationVisible() || CompletionSettings.getInstance(getActiveComponent()).documentationAutoPopup()) { restartDocumentationAutoPopupTimer(); } } /** * Expected to be called from the AWT only. */ @Override public void propertyChange(PropertyChangeEvent e) { assert (SwingUtilities.isEventDispatchThread()); // expected in AWT only boolean cancel = false; JTextComponent component = EditorRegistry.lastFocusedComponent(); if (component != getActiveComponent()) { initActiveProviders(component); JTextComponent activeJtc = getActiveComponent(); if (activeJtc != null) { activeJtc.removeCaretListener(this); activeJtc.removeKeyListener(this); activeJtc.removeFocusListener(this); activeJtc.removeMouseListener(this); Container parent = activeJtc.getParent(); if (parent instanceof JViewport) { JViewport viewport = (JViewport) parent; viewport.removeChangeListener(this); } } if (component != null) { component.addCaretListener(this); component.addKeyListener(this); component.addFocusListener(this); component.addMouseListener(this); Container parent = component.getParent(); if (parent instanceof JViewport) { JViewport viewport = (JViewport) parent; viewport.addChangeListener(this); } } activeComponent = (component != null) ? new WeakReference<>(component) : null; layout.setEditorComponent(getActiveComponent()); stopProfiling(); installKeybindings(); cancel = true; } // Also check document change of an active component Document document = (component != null) ? component.getDocument() : null; if (document != getActiveDocument()) { initActiveProviders(component); if (getActiveDocument() != null) DocumentUtilities.removeDocumentListener(getActiveDocument(), this, DocumentListenerPriority.AFTER_CARET_UPDATE); if (document != null) DocumentUtilities.addDocumentListener(document, this, DocumentListenerPriority.AFTER_CARET_UPDATE); activeDocument = (document != null) ? new WeakReference<>(document) : null; cancel = true; } if (cancel) completionCancel(); } private void initActiveProviders(JTextComponent component) { activeProviders = initActiveProviders(CompletionProvider.class, component); // currently only one async warm up task is allowed at once activeControllerProviders = null; } private <T> T[] initActiveProviders(Class<T> clazz, JTextComponent component) { T[] providers = (component != null) ? getProvidersForComponent(clazz, component, true) : null; if (LOG.isLoggable(Level.FINE)) { StringBuilder sb = new StringBuilder(clazz.getName() + " PROVIDERS:\n"); // NOI18N if (providers != null) { for (int i = 0; i < providers.length; i++) { sb.append("providers["); // NOI18N sb.append(i); sb.append("]: "); // NOI18N sb.append(providers[i].getClass()); sb.append('\n'); } } LOG.fine(sb.toString()); } return providers; } private boolean ensureActiveProviders() { activeProviders = ensureActiveProviders(CompletionProvider.class, activeProviders); activeControllerProviders = ensureActiveProviders(CompletionControllerProvider.class, activeControllerProviders); // allow activeControllerProviders to be null since we have a // DefaultCompletionControllerProvider to fall back on. return activeProviders != null; } private <T> T[] ensureActiveProviders(Class<T> clazz, T[] providers) { if (providers != null) return providers; JTextComponent component = getActiveComponent(); providers = (component != null) ? getProvidersForComponent(clazz, component, false) : null; if (LOG.isLoggable(Level.FINE)) { StringBuilder sb = new StringBuilder(clazz.getName() + " PROVIDERS:\n"); // NOI18N if (providers != null) { for (int i = 0; i < providers.length; i++) { sb.append("providers["); // NOI18N sb.append(i); sb.append("]: "); // NOI18N sb.append(providers[i].getClass()); sb.append('\n'); } } LOG.fine(sb.toString()); } return providers; } private void restartCompletionAutoPopupTimer() { assert (SwingUtilities.isEventDispatchThread()); // expect in AWT only int completionDelay = CompletionSettings.getInstance(getActiveComponent()).completionAutoPopupDelay(); completionAutoPopupTimer.setInitialDelay(completionDelay); completionAutoPopupTimer.restart(); } private void restartDocumentationAutoPopupTimer() { assert (SwingUtilities.isEventDispatchThread()); // expect in AWT only int docDelay = CompletionSettings.getInstance(getActiveComponent()).documentationAutoPopupDelay(); docAutoPopupTimer.setInitialDelay(docDelay); docAutoPopupTimer.restart(); } private <T> T[] getProvidersForComponent(final Class<T> clazz, JTextComponent component, boolean asyncWarmUp) { assert (SwingUtilities.isEventDispatchThread()); if (component == null) return null; 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) { @SuppressWarnings("unchecked") T[] result = (T[])Array.newInstance(clazz, 0); return result; } mimeType = kit.getContentType(); } HashMap<Class<?>, Object[]> providers = providersCache.get(mimeType); if (providers == null) { providers = new HashMap<>(); providersCache.put(mimeType, providers); } if (providers.containsKey(clazz)) { @SuppressWarnings("unchecked") T[] result = (T[])providers.get(clazz); return result; } if (asyncWarmUpTask != null) { if (asyncWarmUp && mimeType != null && mimeType.equals(asyncWarmUpMimeType)) return null; if (!asyncWarmUpTask.cancel()) { asyncWarmUpTask.waitFinished(); } asyncWarmUpTask = null; asyncWarmUpMimeType = null; } final Lookup lookup = MimeLookup.getLookup(MimePath.get(mimeType)); if (asyncWarmUp) { asyncWarmUpMimeType = mimeType; asyncWarmUpTask = RequestProcessor.getDefault().post(new Runnable() { @Override public void run() { lookup.lookupAll(clazz); } }); return null; } Collection<? extends T> col = lookup.lookupAll(clazz); int size = col.size(); @SuppressWarnings("unchecked") T[] ret = size == 0 ? null : col.toArray((T[])Array.newInstance(clazz, size)); providers.put(clazz, 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()); Document doc = comp != null ? comp.getDocument() : null; @SuppressWarnings("null") // doc is only non-null if comp is non-null boolean guardedPos = doc instanceof GuardedDocument && ((GuardedDocument)doc).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()) { SelectedCompletionItem item = layout.getSelectedCompletionItem(); Result result = completionResult; if (item != null && result != null) { sendUndoableEdit(doc, CloneableEditorSupport.BEGIN_COMMIT_GROUP); try { if (compEditable && !guardedPos) { LogRecord r = new LogRecord(Level.FINE, "COMPL_KEY_SELECT"); // NOI18N r.setParameters(new Object[] {e.getKeyChar(), layout.getSelectedIndex(), item.getItem().getClass().getSimpleName()}); result.getController().processKeyEvent(e, item.getItem(), item.isSelected()); if (e.isConsumed()) { uilog(r); return; } } // Call default action if ENTER was pressed and the item is selected // TODO: move this functionality to the CompletionController if (item.isSelected() && e.getKeyCode() == KeyEvent.VK_ENTER && e.getID() == KeyEvent.KEY_PRESSED) { e.consume(); if (guardedPos) { Toolkit.getDefaultToolkit().beep(); } else if (compEditable) { // Consuming completion if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) { // CTRL+ENTER consumeIdentifier(); } LogRecord r = new LogRecord(Level.FINE, "COMPL_KEY_SELECT_DEFAULT"); // NOI18N r.setParameters(new Object[]{'\n', layout.getSelectedIndex(), item.getItem().getClass().getSimpleName()}); result.getController().defaultAction(item.getItem(), item.isSelected()); uilog(r); } return; } } finally { sendUndoableEdit(doc, CloneableEditorSupport.END_COMMIT_GROUP); } } 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) { hideCompletion(false); } // TODO: move this functionality to the CompletionController if (e.getKeyCode() == KeyEvent.VK_TAB && doc.getProperty(CT_HANDLER_DOC_PROPERTY) == null) { e.consume(); if (guardedPos) { Toolkit.getDefaultToolkit().beep(); } else if (compEditable && e.getID() == KeyEvent.KEY_PRESSED) insertCommonPrefix(); return; } } layout.processKeyEvent(e); } static void sendUndoableEdit(Document d, UndoableEdit ue) { if(d instanceof AbstractDocument) { UndoableEditListener[] uels = ((AbstractDocument)d).getUndoableEditListeners(); UndoableEditEvent ev = new UndoableEditEvent(d, ue); for(UndoableEditListener uel : uels) { uel.undoableEditHappened(ev); } } } private void completionQuery(boolean refreshedQuery, boolean delayQuery, int queryType) { Result newCompletionResult = this.new Result(activeProviders.length); synchronized (this) { assert (completionResult == null); completionResult = newCompletionResult; } List<CompletionResultSetImpl> 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 && CompletionSettings.getInstance(getActiveComponent()).completionAutoPopupDelay() > 0) { restartCompletionAutoPopupTimer(); } else { pleaseWaitTimer.restart(); this.refreshedQuery = refreshedQuery; getActiveComponent().putClientProperty("completion-active", Boolean.TRUE); //NOI18N queryResultSets(completionResultSets); newCompletionResult.queryInvoked(); } } else { completionCancel(); if (explicitQuery) { layout.showCompletion(Collections.singletonList(NO_SUGGESTIONS), Collections.<CompletionItem>emptyList(), null, -1, CompletionImpl.this, null, null, FALLBACK_COMPLETION_CONTROLLER, CompletionController.Selection.DEFAULT); } pleaseWaitDisplayed = false; stopProfiling(); } } /** * Called from caretUpdate() to refresh the completion result after caret move. * <br> * 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(true); } } private void completionCancel() { Result oldCompletionResult; synchronized (this) { oldCompletionResult = completionResult; completionResult = null; } if (oldCompletionResult != null) { oldCompletionResult.cancel(); } } /** * Consumes identifier part of text behind caret upto first non-identifier * char. */ private void consumeIdentifier() { JTextComponent comp = getActiveComponent(); BaseDocument doc = (BaseDocument) comp.getDocument(); int initCarPos = comp.getCaretPosition(); int carPos = initCarPos; boolean nonChar = false; char c; try { while(nonChar == false) { c = doc.getChars(carPos, 1)[0]; if(!Character.isJavaIdentifierPart(c)) { nonChar = true; } carPos++; } doc.remove(initCarPos, carPos - initCarPos -1); } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } /** * Called from dispatchKeyEvent() to insert prefix common to all items in the * completion result after TAB.<br> * 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 (CompletionResultSetImpl resultSet : localCompletionResult.getResultSets()) { List<? extends CompletionItem> resultItems = resultSet.getItems(); if (resultItems.size() > 0) { if (anchorOffset >= -1) { if (anchorOffset > -1 && anchorOffset != resultSet.getAnchorOffset()) anchorOffset = -2; else anchorOffset = resultSet.getAnchorOffset(); } for (CompletionItem item : resultItems) { CharSequence text = item.getInsertPrefix(); if (text == null) { commonText = null; break outer; } if (commonText == null) { commonText = text; } else { // Get the largest common part if (text.length() < commonText.length()) commonText = commonText.subSequence(0, text.length()); for (int commonInd = 0; commonInd < commonText.length(); 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()) { final int finalAnchorOffset = anchorOffset; final int finalCaretOffset = caretOffset; final CharSequence finalCommonText = commonText; final Document doc = getActiveDocument(); Runnable operation = new Runnable() { @Override public void run() { try { doc.remove(finalAnchorOffset, finalCaretOffset - finalAnchorOffset); doc.insertString(finalAnchorOffset, finalCommonText.toString(), null); } catch (BadLocationException e) { } } }; // Insert the missing end part of the prefix if (doc instanceof BaseDocument) { ((BaseDocument)doc).runAtomic(operation); } else { operation.run(); } return; } } SelectedCompletionItem item = layout.getSelectedCompletionItem(); if (item != null) localCompletionResult.getController().defaultAction(item.getItem(), item.isSelected()); } /** * May be called from any thread but it will be rescheduled into AWT. */ public void showCompletion() { autoModEndOffset = -1; showCompletion(true, false, false, CompletionProvider.COMPLETION_QUERY_TYPE); } private void showCompletion(boolean explicitQuery, boolean refreshedQuery, 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; } LogRecord r = new LogRecord(Level.FINE, "COMPL_INVOCATION"); // NOI18N r.setParameters(new Object[] {explicitQuery}); uilog(r); this.explicitQuery = explicitQuery; if (ensureActiveProviders()) { 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(refreshedQuery, 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(final Result result) { pleaseWaitTimer.stop(); stopProfiling(); // Compute total count of the result sets int declarationItemsSize = 0; int size = 0; int qType = 0; boolean hasAdditionalItems = false; final StringBuilder hasAdditionalItemsText = new StringBuilder(); List<CompletionResultSetImpl> completionResultSets = result.getResultSets(); for (int i = completionResultSets.size() - 1; i >= 0; i--) { CompletionResultSetImpl resultSet = completionResultSets.get(i); declarationItemsSize += resultSet.getDeclarationItems().size(); size += resultSet.getItems().size(); qType = resultSet.getQueryType(); if (resultSet.hasAdditionalItems()) { hasAdditionalItems = true; String s = resultSet.getHasAdditionalItemsText(); if (s != null) hasAdditionalItemsText.append(s); } } // Collect and sort the gathered completion items List<CompletionItem> resultItems = new ArrayList<>(size); String title = null; int anchorOffset = -1; if (size > 0) { for (int i = 0; i < completionResultSets.size(); i++) { CompletionResultSetImpl resultSet = completionResultSets.get(i); List<? extends CompletionItem> items = resultSet.getItems(); if (items.size() > 0) { resultItems.addAll(items); if (title == null) title = resultSet.getTitle(); if (anchorOffset == -1) anchorOffset = resultSet.getAnchorOffset(); } } } final ArrayList<CompletionItem> sortedResultItems = new ArrayList<>(size = resultItems.size()); if (size > 0) { result.getController().sortItems(resultItems, 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++; } } List<CompletionItem> declarationItems = new ArrayList<>(declarationItemsSize); if (declarationItemsSize > 0) { for (int i = 0; i < completionResultSets.size(); i++) { CompletionResultSetImpl resultSet = completionResultSets.get(i); List<? extends CompletionItem> items = resultSet.getDeclarationItems(); if (!items.isEmpty()) { declarationItems.addAll(items); } } } final ArrayList<CompletionItem> sortedDeclarationItems = new ArrayList<>(declarationItemsSize = declarationItems.size()); if (declarationItemsSize > 0) { result.getController().sortItems(resultItems, getSortType()); sortedDeclarationItems.addAll(declarationItems); } final boolean noSuggestions = sortedResultItems.isEmpty() && declarationItems.isEmpty(); if (noSuggestions) { if (hasAdditionalItems && (qType & CompletionProvider.COMPLETION_ALL_QUERY_TYPE) != CompletionProvider.COMPLETION_ALL_QUERY_TYPE && !this.refreshedQuery) { showCompletion(this.explicitQuery, this.refreshedQuery, false, CompletionProvider.COMPLETION_ALL_QUERY_TYPE); return; } if (!explicitQuery) { hideCompletion(false); 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() { @Override public void run() { synchronized(CompletionImpl.this) { if (result != completionResult) return; } JTextComponent c = getActiveComponent(); Document doc = c.getDocument(); CompletionSettings cs = CompletionSettings.getInstance(c); int caretOffset = c.getSelectionStart(); CompletionController.Selection selection = result.getController().getSelection(sortedResultItems, sortedDeclarationItems); // the CompletionController should be returning a valid selection assert (sortedResultItems.isEmpty() || selection.getIndex() >= 0 && selection.getIndex() < sortedResultItems.size()); if (selection.getIndex() < 0 || selection.getIndex() > sortedResultItems.size()) { selection = CompletionController.Selection.DEFAULT; } if (selection.isUnique() && !refreshedQuery && explicitQuery && cs.completionInstantSubstitution() && c.isEditable() && !(doc instanceof GuardedDocument && ((GuardedDocument)doc).isPosGuarded(caretOffset))) { sendUndoableEdit(doc, CloneableEditorSupport.BEGIN_COMMIT_GROUP); try { if (result.getController().instantSubstitution(sortedResultItems.get(selection.getIndex()))) return; } finally { sendUndoableEdit(doc, CloneableEditorSupport.END_COMMIT_GROUP); } } c.putClientProperty("completion-visible", Boolean.TRUE); layout.showCompletion( noSuggestions ? Collections.singletonList(NO_SUGGESTIONS) : sortedResultItems, sortedDeclarationItems, displayTitle, displayAnchorOffset, CompletionImpl.this, displayAdditionalItems ? hasAdditionalItemsText.toString() : null, displayAdditionalItems ? completionShortcut : null, result.getController(), selection); pleaseWaitDisplayed = false; stopProfiling(); // Show documentation as well if set by default if (cs.documentationAutoPopup()) { if (noSuggestions) { docAutoPopupTimer.stop(); // Ensure the popup timer gets stopped documentationCancel(); layout.hideDocumentation(); } else { restartDocumentationAutoPopupTimer(); } } } }; runInAWT(requestShowRunnable); } /** * 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(); stopProfiling(); boolean hidePerformed = layout.hideCompletion(); pleaseWaitDisplayed = false; JTextComponent jtc = getActiveComponent(); if (!completionOnly && hidePerformed && CompletionSettings.getInstance(jtc).documentationAutoPopup()) { hideDocumentation(true); } if (jtc != null) { jtc.putClientProperty("completion-visible", Boolean.FALSE); jtc.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 (ensureActiveProviders()) { 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() { @Override 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 = this.new Result(1); // Estimate for selected item only synchronized (this) { assert (docResult == null); docResult = newDocumentationResult; } List<CompletionResultSetImpl> documentationResultSets = docResult.getResultSets(); CompletionTask docTask; SelectedCompletionItem selectedItem = layout.getSelectedCompletionItem(); if (selectedItem != null) { lastSelectedItem = new WeakReference<>(selectedItem.getItem()); docTask = selectedItem.getItem().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.getInstance(getActiveComponent()).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 (ensureActiveProviders()) { 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() { @Override public void run() { if (resultSet != null) { layout.showToolTip( resultSet.getToolTip(), resultSet.getAnchorOffset()); } else { hideToolTip(); } } }); } /** * May be called in AWT only. */ private void toolTipQuery() { Result newToolTipResult = this.new Result(1); synchronized (this) { assert (toolTipResult == null); toolTipResult = newToolTipResult; } List<CompletionResultSetImpl> toolTipResultSets = newToolTipResult.getResultSets(); CompletionTask toolTipTask; SelectedCompletionItem selectedItem = layout.getSelectedCompletionItem(); if (selectedItem != null && (toolTipTask = selectedItem.getItem().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(false); } } /** * 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) { // This method is implemented due to the issue // #25715 - Attempt to search keymap for the keybinding that logically corresponds to the action 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) { return keys; } else { // try kit's keymap Keymap km2 = ((BaseKit)kit).getKeymap(); KeyStroke[] keys2 = km2.getKeyStrokesForAction(a); if (keys2 != null && keys2.length > 0) { return keys2; } } } } } } return new KeyStroke[0]; } private void installKeybindings() { actionMap = new ActionMap(); inputMap = new InputMap(); completionShortcut = null; // Register completion show kbs.allInstances(); // in order to make Lookup.Result active and fire events KeyStroke[] keys = findEditorKeys(ExtKit.completionShowAction); for (int i = 0; i < keys.length; i++) { inputMap.put(keys[i], COMPLETION_SHOW); if (completionShortcut == null) { completionShortcut = getKeyStrokeAsText(keys[i]); } } actionMap.put(COMPLETION_SHOW, new CompletionShowAction(CompletionProvider.COMPLETION_QUERY_TYPE)); // Register all completion show keys = findEditorKeys(ExtKit.allCompletionShowAction); for (int i = 0; i < keys.length; i++) { inputMap.put(keys[i], COMPLETION_ALL_SHOW); } actionMap.put(COMPLETION_ALL_SHOW, new CompletionShowAction(CompletionProvider.COMPLETION_ALL_QUERY_TYPE)); // Register documentation show keys = findEditorKeys(ExtKit.documentationShowAction); for (int i = 0; i < keys.length; i++) { inputMap.put(keys[i], DOC_SHOW); } actionMap.put(DOC_SHOW, new DocShowAction()); // Register tooltip show keys = findEditorKeys(ExtKit.completionTooltipShowAction); for (int i = 0; i < keys.length; i++) { inputMap.put(keys[i], TOOLTIP_SHOW); } actionMap.put(TOOLTIP_SHOW, new ToolTipShowAction()); } private static String getKeyStrokeAsText (KeyStroke keyStroke) { int modifiers = keyStroke.getModifiers (); StringBuilder sb = new StringBuilder (); 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 (org.openide.util.Utilities.keyToString ( KeyStroke.getKeyStroke (keyStroke.getKeyCode (), 0) )); sb.append('\''); return sb.toString (); } /** * Notify that a particular completion result set has just been finished. * <br> * This method may be called from any thread. */ void finishNotify(CompletionResultSetImpl finishedResult) { Result localResult; boolean finished = false; switch (finishedResult.getQueryType() & CompletionProvider.RESERVED_QUERY_MASK) { 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<CompletionResultSetImpl> resultSets) { for (int i = resultSets.size() - 1; i >= 0; i--) { CompletionResultSetImpl result = resultSets.get(i); if (!result.isFinished()) { if (LOG.isLoggable(Level.FINE)) { LOG.log(Level.FINE, "CompletionTask: {0} not finished yet\n", result.getTask()); // NOI18N } return false; } } if (LOG.isLoggable(Level.FINE)) { LOG.fine("----- All tasks finished -----\n"); } return true; } /** * Find first result that has non-null documentation or tooltip * depending on its query type. * <br> * The method assumes that all the resultSets are already finished. */ private static CompletionResultSetImpl findFirstValidResult(List<CompletionResultSetImpl> resultSets) { for (int i = 0; i < resultSets.size(); i++) { CompletionResultSetImpl result = resultSets.get(i); switch (result.getQueryType() & CompletionProvider.RESERVED_QUERY_MASK) { 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); } // .......................................................................... /** * Workaround for http://netbeans.org/bugzilla/show_bug.cgi?id=223290 . * * Client needs to explicitly repaint its CompletionItem-s when their full * state is computation is finished in a background thread. */ public void repaintCompletionView() { layout.repaintCompletionView(); } private final class CompletionShowAction extends AbstractAction { private int queryType; private CompletionShowAction(int queryType) { this.queryType = queryType; } @Override public void actionPerformed(ActionEvent e) { autoModEndOffset = -1; showCompletion(true, false, false, queryType); } } private final class DocShowAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { showDocumentation(); } } private final class ToolTipShowAction extends AbstractAction { @Override 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; } @Override public void run() { switch (opCode) { case SHOW_COMPLETION: showCompletion(explicitQuery, false, 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<CompletionResultSetImpl> resultSets) { for (int i = 0; i < resultSets.size(); i++) { CompletionResultSetImpl resultSet = resultSets.get(i); resultSet.getTask().query(resultSet.getResultSet()); } } private static void createRefreshResultSets(List<CompletionResultSetImpl> resultSets, Result refreshResult) { List<CompletionResultSetImpl> 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<CompletionResultSetImpl> 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) { ErrorManager.getDefault().notify(ex); } } private static void cancelResultSets(List<CompletionResultSetImpl> resultSets) { int size = resultSets.size(); for (int i = 0; i < size; i++) { CompletionResultSetImpl result = resultSets.get(i); result.markInactive(); result.getTask().cancel(); } } /** * Result holding list of completion result sets. * <br> * Initially the result is in unprepared state which allows the holding * thread to add the result sets and start the tasks. * <br> * If another thread calls cancel() it has no effect except setting a flag * that is returned from the prepared() method. * <br> * If the result is finished then cancelling physically cancels the result sets. */ final class Result { private final List<CompletionResultSetImpl> resultSets; private boolean invoked; private boolean cancelled; private boolean beforeQuery = true; private CompletionController controller; Result(int resultSetsSize) { resultSets = new ArrayList<>(resultSetsSize); } /** * Get the contained resultSets. * * @return non-null resultSets. */ List<CompletionResultSetImpl> getResultSets() { return resultSets; } CompletionController getController() { synchronized (resultSets) { if (controller == null) { if (resultSets.isEmpty()) { return FALLBACK_COMPLETION_CONTROLLER; } JTextComponent component = getActiveComponent(); List<CompletionTask> tasks = new ArrayList<>(); List<Integer> queryTypes = new ArrayList<>(); for (CompletionResultSetImpl resultSet : resultSets) { tasks.add(resultSet.getTask()); queryTypes.add(resultSet.getQueryType()); } CompletionControllerProvider[] providers = activeControllerProviders; if (providers != null) { for (CompletionControllerProvider provider : providers) { controller = provider.createController(component, tasks, queryTypes); if (controller != null) { break; } } } if (controller == null) { CompletionControllerProvider provider = DefaultCompletionControllerProvider.INSTANCE; controller = provider.createController(component, tasks, queryTypes); } } return controller; } } /** * Cancel the resultSets. * <br> * If the result is not prepared a flag that the result * was cancelled is turned on (and later returned from prepared()). * <br> * 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 = CompletionImpl.this.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(boolean docCancel) { refreshResultSets(getResultSets(), beforeQuery); if (!beforeQuery) { queryInvoked(); synchronized (CompletionImpl.this) { if (completionResult != null) { if (!isAllResultsFinished(completionResult.getResultSets())) { if (docCancel) documentationCancel(); pleaseWaitTimer.restart(); } } } } } } 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); } private static void initializeProfiling(long since) { boolean devel = false; assert devel = true; if (!devel) { return; } synchronized (CompletionImpl.class) { stopProfiling(); profile = new CompletionImplProfile(since); } } private static synchronized void stopProfiling() { if (profile != null) { profile.stop(); profile = null; } } }