com.aptana.ide.editors.unified.contentassist.UnifiedContentAssistProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.aptana.ide.editors.unified.contentassist.UnifiedContentAssistProcessor.java

Source

/**
 * This file Copyright (c) 2005-2008 Aptana, Inc. This program is
 * dual-licensed under both the Aptana Public License and the GNU General
 * Public license. You may elect to use one or the other of these licenses.
 * 
 * This program is distributed in the hope that it will be useful, but
 * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
 * NONINFRINGEMENT. Redistribution, except as permitted by whichever of
 * the GPL or APL you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or modify this
 * program under the terms of the GNU General Public License,
 * Version 3, as published by the Free Software Foundation.  You should
 * have received a copy of the GNU General Public License, Version 3 along
 * with this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 * 
 * Aptana provides a special exception to allow redistribution of this file
 * with certain other free and open source software ("FOSS") code and certain additional terms
 * pursuant to Section 7 of the GPL. You may view the exception and these
 * terms on the web at http://www.aptana.com/legal/gpl/.
 * 
 * 2. For the Aptana Public License (APL), this program and the
 * accompanying materials are made available under the terms of the APL
 * v1.0 which accompanies this distribution, and is available at
 * http://www.aptana.com/legal/apl/.
 * 
 * You may view the GPL, Aptana's exception and additional terms, and the
 * APL in the file titled license.html at the root of the corresponding
 * plugin containing this source file.
 * 
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.ide.editors.unified.contentassist;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.swt.graphics.Image;
import org.osgi.framework.Bundle;

import com.aptana.ide.core.CoreStrings;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.core.PluginUtils;
import com.aptana.ide.core.StringUtils;
import com.aptana.ide.core.ui.SWTUtils;
import com.aptana.ide.editors.UnifiedEditorsPlugin;
import com.aptana.ide.editors.preferences.IPreferenceConstants;
import com.aptana.ide.editors.unified.IUnifiedViewer;
import com.aptana.ide.lexer.Lexeme;
import com.aptana.ide.lexer.LexemeList;
import com.aptana.ide.parsing.IOffsetMapper;

/**
 * Base class for all code assist processors. Adds on required
 * methods for usage later
 * @author Ingo Muschenetz
 *
 */
public abstract class UnifiedContentAssistProcessor implements IUnifiedContentAssistProcessor {

    /**
     * METADATA_ID
     */
    public static final String USERAGENT_ID = "userAgent"; //$NON-NLS-1$

    /**
     * METADATA_ID
     */
    public static final String USERAGENT_ELEMENT = "user-agent"; //$NON-NLS-1$

    /**
     * ATTR_USER_AGENT_NAME
     */
    public static final String ATTR_USER_AGENT_NAME = "name"; //$NON-NLS-1$

    /**
     * ATTR_USER_AGENT_ID
     */
    public static final String ATTR_USER_AGENT_ID = "id"; //$NON-NLS-1$

    /**
     * ATTR_ICON
     */
    public static final String ATTR_ICON = "icon"; //$NON-NLS-1$

    /**
     * ATTR_ICON
     */
    public static final String ATTR_ICON_DISABLED = "icon-disabled"; //$NON-NLS-1$

    /**
     * Has code assist been "forced" via a hotkey?
     */
    //protected boolean hotkeyActivated = false;

    /**
     * Has code assist been "forced" during idle?
     */
    //protected boolean idleActivated = false;

    /**
     * The unified source viewer. contains hooks for indicating we are manually invoking
     * code assist
     */
    protected IUnifiedViewer unifiedViewer = null;

    /**
     * The default code assist trigger character, if we can't find a 
     * better match
     */
    public static char DEFAULT_CHARACTER = '\0';

    /**
     * An array of ICompletionProposalContributors
     */
    private List _contributors = new ArrayList();

    /**
     * The list of agent images
     */
    private static HashMap agentImages = new HashMap();

    /**
     * The lsit of agent names
     */
    private static List agentNames = new ArrayList();

    static {
        try {
            loadUserAgents();
        } catch (Exception ex) {
            IdeLog.logError(UnifiedEditorsPlugin.getDefault(),
                    Messages.UnifiedContentAssistProcessor_ERR_UnableToLoadUserAgents, ex);
        }
    }

    /**
     * Returns the string array of user agents
     * @return String[]
     */
    public static String[] getUserAgents() {
        if (PluginUtils.isPluginLoaded(UnifiedEditorsPlugin.getDefault())) {
            IPreferenceStore prefs = UnifiedEditorsPlugin.getDefault().getPreferenceStore();
            String agents = prefs.getString(IPreferenceConstants.USER_AGENT_PREFERENCE);
            return agents.split(","); //$NON-NLS-1$
        } else {
            return new String[] { Messages.UnifiedContentAssistProcessor_IE,
                    Messages.UnifiedContentAssistProcessor_Mozilla };
        }
    }

    /**
     * Adds a user agent to the list of user agents
     * @param id
     * @param normal
     * @param disabled
     */
    public static void addUserAgent(String id, String name, Image normal, Image disabled) {
        agentImages.put(id, normal);
        agentImages.put(id + "Grey", disabled); //$NON-NLS-1$
        agentNames.add(id);
    }

    /**
     * Loads user agents from the extension points
     */
    private static void loadUserAgents() {
        IExtensionRegistry registry = Platform.getExtensionRegistry();
        if (registry != null) {
            IExtensionPoint extensionPoint = registry.getExtensionPoint(UnifiedEditorsPlugin.ID, USERAGENT_ID);

            if (extensionPoint != null) {
                IExtension[] extensions = extensionPoint.getExtensions();

                for (int i = 0; i < extensions.length; i++) {
                    IExtension extension = extensions[i];
                    IConfigurationElement[] elements = extension.getConfigurationElements();

                    for (int j = 0; j < elements.length; j++) {
                        IConfigurationElement element = elements[j];
                        if (element.getName().equals(USERAGENT_ELEMENT)) {
                            String agentID = element.getAttribute(ATTR_USER_AGENT_ID);
                            String agentName = element.getAttribute(ATTR_USER_AGENT_NAME);
                            String agentIconPath = element.getAttribute(ATTR_ICON);
                            String agentIconDisabledPath = element.getAttribute(ATTR_ICON_DISABLED);
                            if (agentID != null) {
                                IExtension ext = element.getDeclaringExtension();
                                String pluginId = ext.getNamespaceIdentifier();
                                Bundle bundle = Platform.getBundle(pluginId);
                                if (agentIconPath == null || agentIconDisabledPath == null) {
                                    continue;
                                }

                                Image agentIcon = SWTUtils.getImage(bundle, agentIconPath);
                                Image agentIconDisabled = SWTUtils.getImage(bundle, agentIconDisabledPath);
                                if (agentIcon != null && agentIconDisabled != null) {
                                    addUserAgent(agentID, agentName, agentIcon, agentIconDisabled);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Returns the string array of default user agents
     * @return String[]
     */
    public static String[] getDefaultUserAgents() {
        return (String[]) agentNames.toArray(new String[0]);
    }

    /**
     * Returns the image hashtable of loaded user agent images
     * @return HashMap
     */
    public static HashMap getUserAgentImages() {
        return agentImages;
    }

    /**
     * @see com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistProcessor#getCompletionProposalIdleActivationTokens()
     */
    public abstract int[] getCompletionProposalIdleActivationTokens();

    /**
     * The characters that triggers completion proposals (dot for completion, and
     * space for "new XX" in our case)
     * 
     * @return Returns the trigger characters for code completion.
     */
    public char[] getCompletionProposalAutoActivationCharacters() {
        return new char[] {};
    }

    /**
     * The characters that triggers completion proposals (dot for completion, and
     * space for "new XX" in our case)
     * 
     * @return Returns the trigger characters for code completion.
     */
    public int[] getCompletionProposalSeparatorLexemes() {
        return new int[] {};
    }

    /**
     * Characters that trigger tooltip popup help
     * 
     * @return Returns the trigger characters for auto activation.
     */
    public char[] getContextInformationAutoActivationCharacters() {
        // Make context popup automatically after the following characters
        return new char[] {};
    }

    /**
     * The characters that triggers completion proposals (dot for completion, and
     * space for "new XX" in our case). these ones are "private"
     * in that they don't actually trigger a popup
     * 
     * @return Returns the trigger characters for code completion.
     */
    public char[] getCompletionProposalPrivateActivationCharacters() {
        return new char[] {};
    }

    /**
     * The characters that triggers completion proposals (a combination of all activation chars)
     * 
     * @return Returns the trigger characters for code completion.
     */
    public char[] getCompletionProposalAllActivationCharacters() {
        char[] allActivationChars = combine(getCompletionProposalAutoActivationCharacters(),
                getContextInformationAutoActivationCharacters());
        Arrays.sort(allActivationChars);
        return allActivationChars;
    }

    /**
     * Is this a valid location for auto-assisting (based on lexeme, usually)
     * @param viewer 
     * @param offset 
     * @return boolean
     */
    public boolean isValidIdleActivationLocation(ITextViewer viewer, int offset) {
        Lexeme currentLexeme = this.getOffsetMapper().getCurrentLexeme();
        return isValidIdleActivationToken(currentLexeme, getCompletionProposalIdleActivationTokens());
    }

    /**
     * Adds a contributor to the list of contributors
     * @param contributor
     */
    public void addCompletionProposalContributor(ICompletionProposalContributor contributor) {
        _contributors.add(contributor);
    }

    /**
     * Removes a contributor from the list of contributors
     * @param contributor
     */
    public void removeCompletionProposalContributor(ICompletionProposalContributor contributor) {
        _contributors.remove(contributor);
    }

    /**
     * Removes a contributor from the list of contributors
     * 
     * @return ICompletionProposalContributor[]
     */
    public ICompletionProposalContributor[] getCompletionProposalContributors() {
        return (ICompletionProposalContributor[]) _contributors.toArray(new ICompletionProposalContributor[0]);
    }

    /**
     * @param viewer 
     * @param offset 
     * @return ICompletionProposal[]
     * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer,
     *      int)
     */
    public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
        return computeCompletionProposals(viewer, offset, DEFAULT_CHARACTER, false);
    }

    /**
     * @param viewer 
     * @param offset 
     * @param activationChar
     * @return ICompletionProposal[]
     * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer,
     *      int)
     */
    public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset, char activationChar) {
        return computeCompletionProposals(viewer, offset, activationChar, false);
    }

    /**
     * @param viewer 
     * @param offset 
     * @param activationChar
     * @param autoActivated 
     * @return ICompletionProposal[]
     * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer,
     *      int)
     */
    public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset, char activationChar,
            boolean autoActivated) {
        if (viewer instanceof IUnifiedViewer) {
            unifiedViewer = (IUnifiedViewer) viewer;
        }

        Lexeme currentLexeme = this.getOffsetMapper().getCurrentLexeme();
        int currentIndex = computeCurrentLexemeIndex(offset, this.getOffsetMapper().getLexemeList());

        // TODO: This is temporary, as code assist should be correctly determine where the edit point
        // is based upon the caret position, not the position of the CA box. Using the abs prevents this
        // from triggering on backspace. Unsure if that matters
        //int lastOffset = getOffsetMapper().getLastEditOffset();
        //int newOffset = offset;
        //if(offset < lastOffset && (Math.abs(offset - lastOffset) > 1))
        //   newOffset = lastOffset;

        ICompletionProposal[] results = null;

        int sourceLength = 50;
        if (offset < 50) {
            sourceLength = offset;
        }

        String documentSnippet = StringUtils.EMPTY;
        try {
            documentSnippet = viewer.getDocument().get(offset - sourceLength, sourceLength);
        } catch (BadLocationException e) {
            IdeLog.logError(UnifiedEditorsPlugin.getDefault(), CoreStrings.ERROR, e);
        }

        char previousChar = getActivationChar(documentSnippet, documentSnippet.length(),
                getCompletionProposalAllActivationCharacters());

        ArrayList proposals = new ArrayList();
        results = computeInnerCompletionProposals(viewer, offset, currentIndex,
                this.getOffsetMapper().getLexemeList(), activationChar, previousChar);

        if (results != null) {
            proposals.addAll(Arrays.asList(results));
        }

        for (Iterator iter = _contributors.iterator(); iter.hasNext();) {
            ICompletionProposalContributor element = (ICompletionProposalContributor) iter.next();
            ICompletionProposal[] contribs = element.computeCompletionProposals(viewer, offset, currentIndex,
                    this.getOffsetMapper().getLexemeList(), activationChar, previousChar, autoActivated);

            if (contribs != null) {
                proposals.addAll(Arrays.asList(contribs));
            }
        }

        if (unifiedViewer != null) {
            resetViewerState(unifiedViewer);
        }

        ICompletionProposal[] newResults = (ICompletionProposal[]) proposals.toArray(new ICompletionProposal[0]);
        Arrays.sort(newResults, getProposalComparator());
        return newResults;
    }

    /**
     * The proposal comparator
     * 
     * @return Comparator
     */
    public abstract Comparator getProposalComparator();

    /**
     * Resets the viewer state
     * @param unifiedViewer 
     */
    public static void resetViewerState(IUnifiedViewer unifiedViewer) {
        unifiedViewer.setHotkeyActivated(false);
    }

    /**
     * Gets the index of the current lexeme. Modified based on the current separator (or activation characters)
     * @param offset 
     * @param lexemeList
     * @return Returns the index of the current lexeme. Modified based on the current separator (or activation characters)
     */
    public int computeCurrentLexemeIndex(int offset, LexemeList lexemeList) {
        return computeCurrentLexemeIndex(offset, lexemeList, getCompletionProposalSeparatorLexemes());
    }

    /**
     * Gets the index of the current lexeme. Modified based on the current separator (or activation characters)
     * @param offset 
     * @param lexemeList
     * @param separatorTokens 
     * @return Returns the index of the current lexeme. Modified based on the current separator (or activation characters)
     */
    public static int computeCurrentLexemeIndex(int offset, LexemeList lexemeList, int[] separatorTokens) {
        int lexemeIndex = lexemeList.getLexemeFloorIndex(offset);

        if (lexemeIndex <= 0) {
            return lexemeIndex;
        }

        Arrays.sort(separatorTokens);
        Lexeme l = lexemeList.get(lexemeIndex);
        int newIndex = lexemeIndex;
        for (int i = 0; i < separatorTokens.length; i++) {
            int test = separatorTokens[i];
            if (l.typeIndex == test && l.offset == offset) {
                newIndex = newIndex - 1;
                break;
            }
        }

        return newIndex;
    }

    /**
     * Determines what 'tooltip' style highlighting to show. This will be
     * argument insight for methods. This is formatted in
     * HTMLContextInformationValidator (of all places) by implementing the
     * optional IContextInformationPresenter interface.
     * 
     * @param viewer
     * @param offset
     * @return Returns an array of relevant context info.
     */
    public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {

        IContextInformation[] results = null;
        int currentIndex = computeCurrentLexemeIndex(offset, this.getOffsetMapper().getLexemeList());

        results = computeInnerContextInformation(viewer.getDocument().get(), offset, currentIndex,
                this.getOffsetMapper().getLexemeList());

        return results;
    }

    /**
     * Returns the base instance of IOffsetMapper
     * @return IOffsetMapper
     */
    public abstract IOffsetMapper getOffsetMapper();

    /**
     * Generates the completion proposals for code assist
     * @param viewer 
     * @param offset The integer offset of the current insertion point
     * @param position 
     * @param lexemeList 
     * @param activationChar 
     * @param previousChar 
     * @return An array of completion proposals, or null if not valid
     */
    public abstract ICompletionProposal[] computeInnerCompletionProposals(ITextViewer viewer, int offset,
            int position, LexemeList lexemeList, char activationChar, char previousChar);

    /**
     * Generates the context information for code assist
     * @param documentSource The source text of the document
     * @param offset The integer offset of the current insertion point
     * @param position 
     * @param lexemeList 
     * @return An array of completion proposals, or null if not valid
     */
    public abstract IContextInformation[] computeInnerContextInformation(String documentSource, int offset,
            int position, LexemeList lexemeList);

    /**
     * Do we pop up code assist while idle? The lexeme
     * must be of one of the token types in IdleActivationTokens;
     * @param lexeme The lexeme to check
     * @param tokens 
     * @return True if one of the valid idle tokens, false otherwise;
     */
    public static boolean isValidIdleActivationToken(Lexeme lexeme, int[] tokens) {
        if (lexeme == null) {
            return false;
        }

        Arrays.sort(tokens);

        return (Arrays.binarySearch(tokens, lexeme.typeIndex) >= 0);
    }

    /**
     * Selects the first item that is closest to the prefix. Assumes that
     * results is a sorted array
     * @param prefix
     * @param results
     * @return Returns the index of the selected item, or -1 if nothing is selected.
     */
    public static int setSelection(String prefix, ICompletionProposal[] results) {
        int selectedIndex = -1;

        if (prefix == null || prefix.equals(StringUtils.EMPTY) || results == null) {
            return selectedIndex;
        }

        IUnifiedCompletionProposal sensitiveProposal = null;
        IUnifiedCompletionProposal insensitiveProposal = null;
        IUnifiedCompletionProposal suggestedProposal = null;
        for (int i = 0; i < results.length; i++) {
            IUnifiedCompletionProposal cp = (IUnifiedCompletionProposal) results[i];
            String display = cp.getDisplayString();

            int result = display.compareToIgnoreCase(prefix);

            if (result >= 0) {
                if (display.toLowerCase().startsWith(prefix.toLowerCase())) {
                    if (insensitiveProposal == null) {
                        insensitiveProposal = cp;
                        selectedIndex = i;
                    }

                    if (display.startsWith(prefix)) {
                        // we've found an exact case match, so we break;
                        sensitiveProposal = cp;
                        selectedIndex = i;
                        break;
                    }
                } else {
                    suggestedProposal = cp;
                    break;
                }
            }

            // if the item we type is greater than the last proposal,
            // we just suggest the last item in the list
            if (i == results.length - 1) {
                suggestedProposal = cp;
            }
        }

        if (sensitiveProposal != null) {
            sensitiveProposal.setDefaultSelection(true);
        } else if (insensitiveProposal != null) {
            insensitiveProposal.setDefaultSelection(true);
        } else if (suggestedProposal != null) {
            suggestedProposal.setSuggestedSelection(true);
        } else if (results.length > 0) {
            ((IUnifiedCompletionProposal) results[0]).setSuggestedSelection(true);
        }

        return selectedIndex;
    }

    /**
     * Sets as selected the first item that starts with the given prefix. Generally used
     * when the list of results is _not_ sorted.
     * @param prefix
     * @param results
     * @return Returns the index of the selected item, or -1 if nothing is selected.
     */
    public static int setSelectionUnsorted(String prefix, ICompletionProposal[] results) {
        IUnifiedCompletionProposal selectedProposal = null;
        int selectedIndex = -1;

        if (prefix == null || prefix.equals(StringUtils.EMPTY)) {
            return selectedIndex;
        }

        for (int i = 0; i < results.length; i++) {
            IUnifiedCompletionProposal cp = (IUnifiedCompletionProposal) results[i];
            if (cp.getDisplayString().startsWith(prefix)) {
                selectedProposal = cp;
                selectedIndex = i;
                break;
            }
        }

        if (selectedProposal != null) {
            selectedProposal.setDefaultSelection(true);
        } else if (results.length > 0) {
            ((IUnifiedCompletionProposal) results[0]).setDefaultSelection(true);
            selectedIndex = 0;
        }

        return selectedIndex;
    }

    /**
     * getPreviousLexemeOfType
     * 
     * @param offset
     * @param lexemeTypes
     * @param lexemeList
     * @param includeCurrent
     * @return Lexeme
     */
    public static Lexeme getPreviousLexemeOfType(int offset, int[] lexemeTypes, LexemeList lexemeList,
            boolean includeCurrent) {
        return UnifiedContentAssistProcessor.getPreviousLexemeOfType(offset, lexemeTypes, new int[0], lexemeList,
                includeCurrent);
    }

    /**
     * getPreviousLexemeOfType
     * 
     * @param offset
     * @param lexemeTypes
     * @param lexemeTypesToBail
     * @param lexemeList
     * @param includeCurrent
     * @return Lexeme
     */
    public static Lexeme getPreviousLexemeOfType(int offset, int[] lexemeTypes, int[] lexemeTypesToBail,
            LexemeList lexemeList, boolean includeCurrent) {
        Arrays.sort(lexemeTypes);
        Arrays.sort(lexemeTypesToBail);

        Lexeme startLexeme = lexemeList.getFloorLexeme(offset);

        if (!includeCurrent) {
            startLexeme = getPreviousLexeme(offset, lexemeList);
        }

        if (startLexeme == null) {
            return null;
        }

        int index = lexemeList.getLexemeIndex(startLexeme);

        for (int i = index; i >= 0; i--) {
            Lexeme l = lexemeList.get(i);
            if (Arrays.binarySearch(lexemeTypes, l.typeIndex) >= 0) {
                return l;
            }

            if (Arrays.binarySearch(lexemeTypesToBail, l.typeIndex) >= 0) {
                return null;
            }
        }

        return null;
    }

    /**
     * getPreviousLexeme
     * 
     * @param offset
     * @param lexemeList
     * @return Lexeme
     */
    public static Lexeme getPreviousLexeme(int offset, LexemeList lexemeList) {
        if (offset == 0) {
            return null;
        }

        Lexeme floor = lexemeList.getFloorLexeme(offset);
        if (floor.offset == offset) {
            int index = lexemeList.getLexemeIndex(floor);
            if (index > 0) {
                return lexemeList.get(index - 1);
            } else {
                return null;
            }
        } else {
            return floor;
        }
    }

    /**
     * Gets the current activation character. Combines and sorts the arrays
     * 
     * @param source 
     * @param offset
     *            The current offset
     * @param activationCharacters1
     * @param activationCharacters2 
     * @return The current activation character
     */
    public static char getActivationChar(String source, int offset, char[] activationCharacters1,
            char[] activationCharacters2) {

        char[] newArray = new char[activationCharacters1.length + activationCharacters2.length];
        for (int i = 0; i < activationCharacters1.length; i++) {
            newArray[i] = activationCharacters1[i];
        }
        for (int i = 0; i < activationCharacters2.length; i++) {
            newArray[i + activationCharacters1.length] = activationCharacters2[i];
        }

        return getActivationChar(source, offset, newArray);
    }

    /**
     * Gets the current activation character
     * 
     * @param source 
     * @param offset
     *            The current offset
     * @param activationCharacters 
     * @return The current activation character
     */
    public static char getActivationChar(String source, int offset, char[] activationCharacters) {

        Arrays.sort(activationCharacters);
        if (offset > 0) {
            char activationCharacter = getPreviousChar(source, offset);
            // this is a special case as we are using backspace as an activation
            // this can't come from the document
            if (Arrays.binarySearch(activationCharacters, activationCharacter) < 0) {
                activationCharacter = UnifiedContentAssistProcessor.DEFAULT_CHARACTER;
            }
            return activationCharacter;
        } else {
            return DEFAULT_CHARACTER;
        }
    }

    /**
     * Gets the previous character
     * 
     * @param source
     * @param offset
     *            The current offset
     * @return The previous character
     */
    public static char getPreviousChar(String source, int offset) {

        if (source == null) {
            throw new IndexOutOfBoundsException(Messages.UnifiedContentAssistProcessor_StringNotNull);
        }

        if (offset > 0 && offset <= source.length()) {
            char activationCharacter = source.charAt(offset - 1);
            return activationCharacter;
        } else {
            return DEFAULT_CHARACTER;
        }
    }

    /**
     * Returns the current preference store
     * @return The current preference store, or null if not found
     */
    protected abstract IPreferenceStore getPreferenceStore();

    /**
     * Combines two arrays into a single array
     * @param array1 The first array
     * @param array2 The second array
     * @return char[]
     */
    public static char[] combine(char[] array1, char[] array2) {

        char[] newArray = new char[array1.length + array2.length];
        System.arraycopy(array1, 0, newArray, 0, array1.length);
        System.arraycopy(array2, 0, newArray, array1.length, array2.length);
        return newArray;
    }

    /**
     * Is there a proposal that exactly matches the prefix?
     * @param proposal The proposal to test for
     * @param proposals The list of proposals
     * @return containsExactProposalMatch
     */
    public static boolean containsExactProposalMatch(String proposal, ICompletionProposal[] proposals) {
        if (proposals == null) {
            return false;
        }

        for (int i = 0; i < proposals.length; i++) {
            ICompletionProposal prop = proposals[i];
            String displayProp = prop.getDisplayString();
            if (displayProp.equals(proposal)) {
                return true;
            }

            if (prop instanceof IUnifiedCompletionProposal
                    && ((IUnifiedCompletionProposal) prop).getReplaceString().equals(proposal)) {
                return true;
            }

            String tempProposal = proposal;

            if (tempProposal.startsWith("\"") || tempProposal.startsWith("'")) //$NON-NLS-1$ //$NON-NLS-2$
            {
                tempProposal = tempProposal.substring(1);
            }

            if (tempProposal.endsWith("\"") || tempProposal.endsWith("'")) //$NON-NLS-1$ //$NON-NLS-2$
            {
                tempProposal = tempProposal.substring(0, tempProposal.length() - 1);
            }

            if (displayProp.equals(tempProposal)) {
                return true;
            }
        }

        return false;
    }

    /**
     *  Get the user agent documentation
     * @param supportedUserAgents 
     * @param allUserAgents 
     * @return Image[]
     */
    public static Image[] getUserAgentImages(String[] supportedUserAgents, String[] allUserAgents) {
        if (supportedUserAgents.length > 0) {
            ArrayList images = new ArrayList();
            for (int i = 0; i < supportedUserAgents.length; i++) {
                String agentName = supportedUserAgents[i];
                Image image = null;
                for (int j = 0; j < allUserAgents.length; j++) {
                    String ua = allUserAgents[j];
                    if (ua.equals(agentName)) {
                        image = (Image) agentImages.get(agentName);
                    }
                }
                if (image == null) {
                    image = (Image) agentImages.get(agentName + "Grey"); //$NON-NLS-1$
                }

                images.add(image);
            }
            return (Image[]) images.toArray(new Image[0]);
        } else {
            return getGreyUserAgentImages(allUserAgents);
        }
    }

    /**
     * Get a list of all agents, greyed out
     * @param userAgents
     * @return Image[]
     */
    public static Image[] getGreyUserAgentImages(String[] userAgents) {
        ArrayList images = new ArrayList();
        for (int i = 0; i < userAgents.length; i++) {
            String agentName = userAgents[i];
            Image image = (Image) agentImages.get(agentName + "Grey"); //$NON-NLS-1$
            images.add(image);
        }
        return (Image[]) images.toArray(new Image[0]);
    }

    /**
     * Get a list of all agents, default colors
     * @param userAgents
     * @return gets AllUserAgentImages
     */
    public static Image[] getAllUserAgentImages(String[] userAgents) {
        ArrayList images = new ArrayList();
        for (int i = 0; i < userAgents.length; i++) {
            String agentName = userAgents[i];
            Image image = (Image) agentImages.get(agentName);
            images.add(image);
        }
        return (Image[]) images.toArray(new Image[0]);
    }
}