All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.editor.ext.Completion Maven / Gradle / Ivy

/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.editor.ext;

import java.awt.EventQueue;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
import javax.swing.Timer;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;
import javax.swing.event.CaretListener;
import javax.swing.event.CaretEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Settings;
import org.netbeans.editor.SettingsChangeListener;
import org.netbeans.editor.SettingsChangeEvent;
import org.netbeans.editor.SettingsUtil;
import org.netbeans.editor.Utilities;
import org.netbeans.editor.WeakTimerListener;
import javax.swing.text.BadLocationException;
import javax.swing.event.ListSelectionListener;
import org.netbeans.editor.LocaleSupport;
import org.openide.util.RequestProcessor;

/**
* General Completion display formatting and services
*
* @author Miloslav Metelka
* @version 1.00
*/

public class Completion
    implements PropertyChangeListener, SettingsChangeListener, ActionListener {

        
        
    /** Editor UI supporting this completion */
    protected ExtEditorUI extEditorUI;

    /** Completion query providing query support for this completion */
    private CompletionQuery query;

    /** Last result retrieved for completion. It can become null
    * if the document was modified so the replacement position
    * would be invalid.
    */
    private CompletionQuery.Result lastResult;
    
    private boolean keyPressed = false;

    /** Completion view component displaying the completion help */
    private CompletionView view;

    /** Component (usually scroll-pane) holding the view and the title
    * and possibly other necessary components.
    */
    private ExtCompletionPane pane;
    
    private JavaDocPane javaDocPane;
    
    private JDCPopupPanel jdcPopupPanel;

    private boolean autoPopup;

    private int autoPopupDelay;

    private int refreshDelay;

    private boolean instantSubstitution;
    
    Timer timer;
    Timer docChangeTimer;

    private DocumentListener docL;
    private CaretListener caretL;

    private PropertyChangeListener docChangeL;
    
    private int caretPos=-1;

    // old providers was called serialy from AWT, emulate it by RP queue
    private static RequestProcessor serializingRequestProcessor;
    
    // sample property at static initialized, but allow dynamic disabling later
    private static final String PROP_DEBUG_COMPLETION = "editor.debug.completion";  // NOI18N
    private static final boolean DEBUG_COMPLETION = Boolean.getBoolean(PROP_DEBUG_COMPLETION);
    
    // Every asynchronous task can be splitted into several subtasks.
    // The task can between subtasks using simple test determine whether
    // it was not cancelled.
    // It emulates #28475 RequestProcessor enhancement request.
    private CancelableRunnable cancellable = new CancelableRunnable() {
        public void run() {}
    };
    public boolean provokedByAutoPopup;

    public Completion(ExtEditorUI extEditorUI) {
        this.extEditorUI = extEditorUI;

        // Initialize timer
        timer = new Timer(0, new WeakTimerListener(this)); // delay will be set later
        timer.setRepeats(false);

        docChangeTimer = new Timer(0, new WeakTimerListener(new ActionListener(){
            public void actionPerformed(ActionEvent e){
               refreshImpl( false );  //??? why do not we batch them by posting it?
            }
        }));
        docChangeTimer.setRepeats(false);
        
        // Create document listener
        class CompletionDocumentListener implements DocumentListener {
            
           private void processTimer(){
               docChangeTimer.stop();
               setKeyPressed(true);
               invalidateLastResult();
               docChangeTimer.setInitialDelay(refreshDelay);
               docChangeTimer.setDelay(refreshDelay);
               docChangeTimer.start();
           }
           
           public void insertUpdate(DocumentEvent evt) {
               trace("ENTRY insertUpdate"); // NOI18N
               processTimer();
           }

           public void removeUpdate(DocumentEvent evt) {
               trace("ENTRY removeUpdate"); // NOI18N
               processTimer();
           }

           public void changedUpdate(DocumentEvent evt) {
           }
        };        
        docL = new CompletionDocumentListener();


        class CompletionCaretListener implements CaretListener {
            public void caretUpdate( CaretEvent e ) {
                trace("ENTRY caretUpdate"); // NOI18N
                if (!isPaneVisible()){
                    // cancel timer if caret moved
                    cancelRequestImpl();
                }else{
                    //refresh completion only if a pane is already visible
                    refreshImpl( true );
                }
            }
        };        
        caretL = new CompletionCaretListener();
       
        Settings.addSettingsChangeListener(this);

        synchronized (extEditorUI.getComponentLock()) {
            // if component already installed in ExtEditorUI simulate installation
            JTextComponent component = extEditorUI.getComponent();
            if (component != null) {
                propertyChange(new PropertyChangeEvent(extEditorUI,
                                                       ExtEditorUI.COMPONENT_PROPERTY, null, component));
            }

            extEditorUI.addPropertyChangeListener(this);
        }
    }

    public void settingsChange(SettingsChangeEvent evt) {
        Class kitClass = Utilities.getKitClass(extEditorUI.getComponent());

        if (kitClass != null) {
            autoPopup = SettingsUtil.getBoolean(kitClass,
                                                ExtSettingsNames.COMPLETION_AUTO_POPUP,
                                                ExtSettingsDefaults.defaultCompletionAutoPopup);

            autoPopupDelay = SettingsUtil.getInteger(kitClass,
                             ExtSettingsNames.COMPLETION_AUTO_POPUP_DELAY,
                             ExtSettingsDefaults.defaultCompletionAutoPopupDelay);

            refreshDelay = SettingsUtil.getInteger(kitClass,
                                                   ExtSettingsNames.COMPLETION_REFRESH_DELAY,
                                                   ExtSettingsDefaults.defaultCompletionRefreshDelay);

            instantSubstitution = SettingsUtil.getBoolean(kitClass,
                                                   ExtSettingsNames.COMPLETION_INSTANT_SUBSTITUTION,
                                                   ExtSettingsDefaults.defaultCompletionInstantSubstitution);
            
        }
    }

    public void propertyChange(PropertyChangeEvent evt) {
        String propName = evt.getPropertyName();

        if (ExtEditorUI.COMPONENT_PROPERTY.equals(propName)) {
            JTextComponent component = (JTextComponent)evt.getNewValue();
            if (component != null) { // just installed

                settingsChange(null);
                
                BaseDocument doc = Utilities.getDocument(component);
                if (doc != null) {
                    doc.addDocumentListener(docL);
                }

                component.addCaretListener( caretL );
            } else { // just deinstalled
                
                setPaneVisible(false);
                
                component = (JTextComponent)evt.getOldValue();

                BaseDocument doc = Utilities.getDocument(component);
                if (doc != null) {
                    doc.removeDocumentListener(docL);
                }
                
                if( component != null ) {
                    component.removeCaretListener( caretL );
                }
            }

        } else if ("document".equals(propName)) { // NOI18N
            if (evt.getOldValue() instanceof BaseDocument) {
                ((BaseDocument)evt.getOldValue()).removeDocumentListener(docL);
            }
            if (evt.getNewValue() instanceof BaseDocument) {
                ((BaseDocument)evt.getNewValue()).addDocumentListener(docL);
            }

        }

    }

    public CompletionPane getPane() {
        return (CompletionPane) getExtPane();
    }

    public ExtCompletionPane getExtPane() {
        if (pane == null){
            pane = new ScrollCompletionPane(extEditorUI);
        }
        return pane;
    }
    
    protected CompletionView createView() {
        return new ListCompletionView();
    }

    public final CompletionView getView() {
        if (view == null) {
            view = createView();
        }
        return view;
    }

    protected CompletionQuery createQuery() {
        return null;
    }

    public final CompletionQuery getQuery() {
        if (query == null) {
            query = createQuery();
        }
        return query;
    }
    
    public JavaDocPane getJavaDocPane(){
        if (javaDocPane == null){
            javaDocPane = new ScrollJavaDocPane(extEditorUI);
        }
        return javaDocPane;
    }
    
    /**
     * Get panel holding all aids (completion and documentation panes).
     * @return JDCPopupPanel or null
     */
    public final JDCPopupPanel getJDCPopupPanelIfExists() {
        return jdcPopupPanel;
    }

    /**
     * Get panel holding all aids (completion and documentation panes).
     * @return JDCPopupPanel never null
     */ 
    public JDCPopupPanel getJDCPopupPanel(){
        if (jdcPopupPanel == null){
            jdcPopupPanel =  new JDCPopupPanel(extEditorUI, getExtPane(), this);
        }
        return jdcPopupPanel;
    }

    /** Get the result of the last valid completion query or null
    * if there's no valid result available.
    */
    public synchronized final CompletionQuery.Result getLastResult() {
        return lastResult;
    }

    /** Reset the result of the last valid completion query. This
    * is done for example after the document was modified.
    */
    public synchronized final void invalidateLastResult() {
        currentTask().cancel();
        lastResult = null;
        caretPos = -1;
    }
     
    private synchronized void setKeyPressed(boolean value) {
        keyPressed = value;
    }
    
    private synchronized boolean isKeyPressed() {
        return keyPressed;
    }

    public synchronized Object getSelectedValue() {
        if (lastResult != null) {
            int index = getView().getSelectedIndex();
            if (index >= 0 && indexgetPane().isVisible() that forces
    * the creation of the pane.
    */
    public boolean isPaneVisible() {
        return (pane != null && pane.isVisible());
    }

    /** Set the visibility of the view. This method should
    * be used mainly for hiding the completion pane. If used
    * with visible set to true it calls the popup(false).
    */
    public void setPaneVisible(boolean visible) {
        trace("ENTRY setPaneVisible " + visible); // NOI18N
        if (visible) {
            if (extEditorUI.getComponent() != null) {
                popupImpl(false);
            }
        } else {
            if (pane != null) {
                cancelRequestImpl();
                invalidateLastResult();
                getJDCPopupPanel().setCompletionVisible(false);
                caretPos=-1;
            }
        }
    }
    
    public void completionCancel(){
        trace("ENTRY completionCancel"); // NOI18N
        if (pane != null){
            cancelRequestImpl();
            invalidateLastResult();
            caretPos=-1;
        }
    }

    /** Refresh the contents of the view if it's currently visible.
    * @param postRequest post the request instead of refreshing the view
    *   immediately. The ExtSettingsNames.COMPLETION_REFRESH_DELAY
    *   setting stores the number of milliseconds before the view is refreshed.
    */
    public void refresh(boolean postRequest) {
        trace("ENTRY refresh " + postRequest); // NOI18N
        refreshImpl(postRequest);
    }

    private synchronized void refreshImpl(final boolean postRequest) {

        // exit immediatelly
        if (isPaneVisible() == false) return;
        
        class RefreshTask implements Runnable {
            private final boolean batch;
            RefreshTask(boolean batch) {
                this.batch = batch;
            }
            public void run() {
                if (isPaneVisible()) {
                    timer.stop();
                    if (batch) {
                        timer.setInitialDelay(refreshDelay);
                        timer.setDelay(refreshDelay);
                        timer.start();
                    } else {
                        actionPerformed(null);
                    }
                }
            }            
        };
        
        SwingUtilities.invokeLater(new RefreshTask(postRequest));        
    }
    
    /** Get the help and show it in the view. If the view is already visible
    * perform the refresh of the view.
    * @param postRequest post the request instead of displaying the view
    *   immediately. The ExtSettingsNames.COMPLETION_AUTO_POPUP_DELAY
    *   setting stores the number of milliseconds before the view is displayed.
    *   If the user presses a key until the delay expires nothing is shown.
    *   This guarantees that the user which knows what to write will not be
    *   annoyed with the unnecessary help.
    */
    public void popup(boolean postRequest) {
        trace("ENTRY popup " + postRequest); // NOI18N
        popupImpl(postRequest);
    }

    private synchronized void popupImpl( boolean postRequest) {
        if (isPaneVisible()) {
            refreshImpl(postRequest);
        } else {
            timer.stop();
            if (postRequest) {
                timer.setInitialDelay(autoPopupDelay);
                timer.setDelay(autoPopupDelay);
                timer.start();
            } else {
                actionPerformed(null);
            }
        }        
    }
    
    /** Cancel last request for either displaying or refreshing
    * the pane. It resets the internal timer.
    */
    public void cancelRequest() {
        trace("ENTRY cancelRequest"); // NOI18N
        cancelRequestImpl();
    }
    
    private synchronized void cancelRequestImpl() {
        timer.stop();
    }

    /** Called to do either displaying or refreshing of the view.
    * This method can be called either directly or because of the timer has fired.
    * @param evt event describing the timer firing or null
    *   if the method was called directly because of the synchronous
    *   showing/refreshing the view.
    */
    public synchronized void actionPerformed(ActionEvent evt) {

        if (jdcPopupPanel == null) extEditorUI.getCompletionJavaDoc(); //init javaDoc
        
        final JTextComponent component = extEditorUI.getComponent();
        BaseDocument doc = Utilities.getDocument(component);

        if (component != null && doc != null) {
            
            provokedByAutoPopup = evt != null;

            try{
                if((caretPos!=-1) && (Utilities.getRowStart(component,component.getCaret().getDot()) !=
                    Utilities.getRowStart(component,caretPos)) && ((component.getCaret().getDot()-caretPos)>0) ){
                        getJDCPopupPanel().setCompletionVisible(false);
                        caretPos=-1;
                        return;
                }
            }catch(BadLocationException ble){
            }
            
            caretPos = component.getCaret().getDot();

            // show progress view
            class PendingTask extends CancelableRunnable {
                public void run() {
                    if (cancelled()) return;
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            if (cancelled()) return;
                            performWait();
                        }
                    });
                }                
            };

            // perform query and show results
            class QueryTask extends CancelableRunnable {
                private final CancelableRunnable wait;
                private final boolean isPaneVisible;
                public QueryTask(CancelableRunnable wait, boolean isPaneVisible) {
                    this.wait = wait;
                    this.isPaneVisible = isPaneVisible;
                }
                public void run() {
                    if (cancelled()) return;
                    try {
                        performQuery(component);
                    } catch ( ThreadDeath td ) {
                        throw td;
                    } catch (Throwable exc){
                        exc.printStackTrace();
                    }finally {
                        wait.cancel();
                        if (cancelled()) return;
                        SwingUtilities.invokeLater( new Runnable() {
                            public void run() {
                                if (cancelled()) return;
                                CompletionQuery.Result res = lastResult;
                                if (res != null) {
                                    if (instantSubstitution && res.getData().size() == 1 &&
                                        !isPaneVisible && instantSubstitution(caretPos)){
                                            setPaneVisible(false);
                                            return;
                                    }
                                }
                                
                                performResults();
                            }
                        });
                    }
                }
                void cancel() {
                    super.cancel();
                    wait.cancel();
                }
            };
                        
            // update current task: cancel pending task and fire new one

            currentTask().cancel();
            
            RequestProcessor rp;
            boolean reentrantProvider = getQuery() instanceof CompletionQuery.SupportsSpeculativeInvocation;
            if (reentrantProvider) {
                rp = RequestProcessor.getDefault();
            } else {
                rp = getSerialiazingRequestProcessor();
            }

            CancelableRunnable wait = new PendingTask();            
            CancelableRunnable task = new QueryTask(wait, getPane().isVisible());
            currentTask(task);
            if (provokedByAutoPopup == false) {
                RequestProcessor.getDefault().post(wait, 100);
            }
            rp.post(task);
        }
    }

    /**
     * Show wait completion result. Always called from AWT.
     */
    private void performWait() {
        getPane().setTitle(LocaleSupport.getString("ext.Completion.wait"));
        getView().setResult((CompletionQuery.Result)null);
        if (isPaneVisible()) {
            getJDCPopupPanel().refresh();
        } else {
            getJDCPopupPanel().setCompletionVisible(true);
        }        
    }
    
    /**
     * Execute complegtion query subtask
     */
    private void performQuery(final JTextComponent target) {

        BaseDocument doc = Utilities.getDocument(target);
        long start = System.currentTimeMillis();
        try {
            lastResult = getQuery().query( target, caretPos, doc.getSyntaxSupport());
        } finally {
            trace("performQuery took " + (System.currentTimeMillis() - start) + "ms"); // NOI18N
            setKeyPressed(false);
        }
    }

    /**
     * Show result popup. Always called from AWT.
     */
    protected void performResults() {
        // sample
        CompletionQuery.Result res = lastResult;
        if (res != null) {
            
            if (instantSubstitution && res.getData().size() == 1 &&
                !isPaneVisible() && instantSubstitutionImpl(caretPos)) return;

            getPane().setTitle(res.getTitle());
            getView().setResult(res);
            if (isPaneVisible()) {
                getJDCPopupPanel().refresh();
            } else {
                getJDCPopupPanel().setCompletionVisible(true);
            }
        } else {
            getJDCPopupPanel().setCompletionVisible(false);
            
            if (!isKeyPressed()) {
                caretPos=-1;
            } else {
                setKeyPressed(false);
            }
        }        
    }
    
    /** Performs instant text substitution, provided that result contains only one 
     *  item and completion has been invoked at the end of the word.
     *  @param caretPos offset position of the caret
     */
    public boolean instantSubstitution(int caretPos){
        trace("ENTRY instantSubstitution " + caretPos); // NOI18N
        return instantSubstitutionImpl(caretPos);
    }
    
    private synchronized boolean instantSubstitutionImpl(int caretPos){
        if (getLastResult() == null) return false;
        JTextComponent comp = extEditorUI.getComponent();
        try{
            if ((comp == null) || Utilities.getWordEnd(comp,caretPos) > caretPos) return false;
            return getLastResult().substituteText(0, false);
        }catch(BadLocationException ble){
            return false;
        }
    }

    
    /** Substitute the document's text with the text
    * that is appopriate for the selection
    * in the view. This function is usually triggered
    * upon pressing the Enter key.
    * @return true if the substitution was performed
    *  false if not.
    */
    public synchronized boolean substituteText( boolean shift ) {        
        trace("ENTRY substituteText " + shift); // NOI18N
        if (lastResult != null) {
            int index = getView().getSelectedIndex();
            if (index >= 0) {
                lastResult.substituteText(index, shift );
            }
            return true;
        } else {
            return false;
        }
    }

    /** Substitute the text with the longest common
    * part of all the entries appearing in the view.
    * This function is usually triggered
    * upon pressing the Tab key.
    * @return true if the substitution was performed
    *  false if not.
    */
    public synchronized boolean substituteCommonText() {
        trace("ENTRY substituteCommonText"); // NOI18N
        if (lastResult != null) {
            int index = getView().getSelectedIndex();
            if (index >= 0) {
                lastResult.substituteCommonText(index);
            }
            return true;
        } else {
            return false;
        }
    }

    
    // Task management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    
    /**
     * Make given task current.
     */
    private void currentTask(CancelableRunnable task) {
        cancellable = task;
    }
    
    /**
     * Get current task
     */
    private CancelableRunnable currentTask() {
        return cancellable;
    }
    
    /**
     * Multistage task can test its cancel status after every atomic
     * (non-cancellable) stage.
     */
    abstract class CancelableRunnable implements Runnable {
        private boolean cancelled = false;
                
        boolean cancelled() {
            return cancelled;
        }
        
        void cancel() {
            cancelled = true;
        }
    }

    /**
     * Get serializing request processor.
     */
    private synchronized RequestProcessor getSerialiazingRequestProcessor() {
        if (serializingRequestProcessor == null) {
            serializingRequestProcessor = new RequestProcessor("editor.completion", 1);// NOI18N
        }
        return serializingRequestProcessor;
    }
    
    // Debug support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    private static void trace(String msg) {
        if (DEBUG_COMPLETION && Boolean.getBoolean(PROP_DEBUG_COMPLETION)) {
            synchronized (System.err) {
                System.err.println(msg);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy