/* * Copyright (c) 2003-2004 Christopher Lenz and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * Christopher Lenz - initial API and implementation * * $Id: CssContentAssistProcessor.java,v 1.1 2004-09-02 18:11:48 jsurfer Exp $ */ package net.sourceforge.phpeclipse.css.ui.internal.text; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import net.sourceforge.phpeclipse.core.model.ISourceReference; import net.sourceforge.phpeclipse.css.core.internal.text.CssTextUtils; import net.sourceforge.phpeclipse.css.core.model.IAtRule; import net.sourceforge.phpeclipse.css.core.model.IDeclaration; import net.sourceforge.phpeclipse.css.core.model.IPropertyInfo; import net.sourceforge.phpeclipse.css.core.model.IRule; import net.sourceforge.phpeclipse.css.core.model.IStyleRule; import net.sourceforge.phpeclipse.css.core.model.IStyleSheet; import net.sourceforge.phpeclipse.css.core.profiles.IProfile; import net.sourceforge.phpeclipse.css.ui.CssUI; import net.sourceforge.phpeclipse.css.ui.internal.CssDocumentProvider; import net.sourceforge.phpeclipse.css.ui.internal.CssUIPreferences; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContentAssistProcessor; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.contentassist.IContextInformationValidator; import org.eclipse.swt.graphics.Image; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; /** * Content assist processor for the CSS editor. * * TODO If a completion proposal is requested before the style sheet has been * reconciled, we might not get any proposals, or they might be false. */ public class CssContentAssistProcessor implements IContentAssistProcessor { // Constants --------------------------------------------------------------- private static final String AUTOACTIVATION_TRIGGERS = CssUIPreferences.CONTENTASSIST_AUTOACTIVATION_TRIGGERS; private static final String ORDER_PROPOSALS = CssUIPreferences.CONTENTASSIST_ORDER_PROPOSALS; // Instance Variables ------------------------------------------------------ /** * The preference store. */ private IPreferenceStore store; /** * The current CSS profile. */ private IProfile profile; /** * The associated text editor, if any. */ private ITextEditor editor; // Constructors ------------------------------------------------------------ /** * Constructor. * * @param profile The CSS profile to use */ public CssContentAssistProcessor(IPreferenceStore store, IProfile profile, ITextEditor editor) { this.store = store; this.profile = profile; this.editor = editor; } // IContentAssistProcessor ------------------------------------------------- /* * @see IContentAssistProcessor#computeCompletionProposals */ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int documentOffset) { List retVal = Collections.EMPTY_LIST; IDocument document = viewer.getDocument(); try { String prefix = getPrefix(document, documentOffset); if (prefix.startsWith("@")) { //$NON-NLS-1$ retVal = proposeAtKeywords(document, documentOffset, prefix.substring(1)); } else if (prefix.startsWith(":")) { //$NON-NLS-1$ retVal = proposePseudoClasses(document, documentOffset, prefix.substring(1)); } else { retVal = proposeProperties(document, documentOffset, prefix); } if ((store != null) && store.getBoolean(ORDER_PROPOSALS)) { sortProposals(retVal); } } catch (BadLocationException e) { CssUI.log( "Unable to compute completion proposals", e); //$NON-NLS-1$ } return (ICompletionProposal[]) retVal.toArray( new ICompletionProposal[retVal.size()]); } /* * @see IContentAssistProcessor#computeContextInformation */ public IContextInformation[] computeContextInformation(ITextViewer viewer, int documentOffset) { return new IContextInformation[0]; } /* * @see IContentAssistProcessor#getCompletionProposalAutoActivationCharacters */ public char[] getCompletionProposalAutoActivationCharacters() { String chars = store.getString(AUTOACTIVATION_TRIGGERS); if (chars == null) { return null; } return chars.toCharArray(); } /* * @see IContentAssistProcessor#getContextInformationAutoActivationCharacters */ public char[] getContextInformationAutoActivationCharacters() { return null; } /* * @see IContentAssistProcessor#getErrorMessage */ public String getErrorMessage() { return null; } /* * @see IContentAssistProcessor#getContextInformationValidator */ public IContextInformationValidator getContextInformationValidator() { return null; } // Private Methods --------------------------------------------------------- /** * Returns the part of the word immediately before the position at which * content assist was requested. The prefix is lower-cased before it is * returned, to enable case-insensitive matching. * * @param document the document * @param offset the offset into the document * @return the prefix */ private String getPrefix(IDocument document, int offset) { try { int startPos = offset; while (startPos > 0) { char c = document.getChar(startPos - 1); if (!CssTextUtils.isCssIdentifierPart(c) && (c != '@') && (c != ':')) { break; } startPos--; if ((c == '@') || (c == ':')) { break; } } if (startPos < offset) { return document.get(startPos, offset - startPos).toLowerCase(); } } catch (BadLocationException e) { e.printStackTrace(); } return ""; //$NON-NLS-1$ } /** * Returns the parsed model of the document loaded in the editor, or * null if the editor hasn't been set or the model couldn't be * retrieved. * * @return the parsed model */ private IStyleSheet getStyleSheet() { if (editor != null) { IDocumentProvider provider = editor.getDocumentProvider(); if (provider instanceof CssDocumentProvider) { return ((CssDocumentProvider) provider).getStyleSheet( editor.getEditorInput()); } } return null; } public boolean isAtKeyword(IDocument document, int offset) throws BadLocationException { IStyleSheet styleSheet = getStyleSheet(); if (styleSheet != null) { IRule rule = styleSheet.getRuleAt(offset); if (rule instanceof IAtRule) { ISourceReference name = ((IAtRule) rule).getName(); if (name != null) { IRegion region = name.getSourceRegion(); for (int j = region.getOffset(); j < offset; j++) { char ch = document.getChar(j); if (!CssTextUtils.isCssIdentifierPart(ch)) { return false; } } return true; } } else if (rule == null) { return true; } } return false; } /** * Determines whether the document contains a property at the specified * offset, or at least whether a property is theoretically allowed at that * offset. * * @param document the document * @param offset the offset into the document * @return true if the specified offset is a legal position for * a property, false otherwise * @throws BadLocationException if there was a problem accessing the * document */ public boolean isProperty(IDocument document, int offset) throws BadLocationException { IStyleSheet styleSheet = getStyleSheet(); if (styleSheet != null) { IRule rule = styleSheet.getRuleAt(offset); if (rule != null) { IDeclaration declaration = rule.getDeclarationAt(offset); if (declaration != null) { IRegion region = declaration.getSourceRegion(); for (int j = region.getOffset(); j < offset; j++) { if (document.getChar(j) == ':') { return false; } } return true; } else { IRegion region = rule.getSourceRegion(); for (int j = region.getOffset(); j < offset; j++) { if (document.getChar(j) == '{') { return true; } } } } } return false; } /** * Determines whether the document contains a selector at the specified * offset, or at least whether a selector is theoretically allowed at that * offset. */ public boolean isSelector(IDocument document, int offset) throws BadLocationException { IStyleSheet styleSheet = getStyleSheet(); if (styleSheet != null) { IRule rule = styleSheet.getRuleAt(offset); if (rule instanceof IStyleRule) { ISourceReference selector = ((IStyleRule) rule).getSelector(); if (selector != null) { IRegion region = selector.getSourceRegion(); for (int j = region.getOffset(); j < offset; j++) { if (document.getChar(j) == '{') { return false; } } return true; } } else if (rule == null) { return true; } } return false; } private List proposeAtKeywords(IDocument document, int offset, String prefix) throws BadLocationException { List proposals = new ArrayList(); if (isAtKeyword(document, offset)) { Image icon = CssUI.getDefault().getImageRegistry().get( CssUI.ICON_AT_RULE); Collection atRuleNames = profile.getAtKeywords(); for (Iterator i = atRuleNames.iterator(); i.hasNext(); ) { String atRuleName = (String) i.next(); if (atRuleName.startsWith(prefix)) { ICompletionProposal proposal = new CompletionProposal( atRuleName, offset - prefix.length(), prefix.length(), atRuleName.length(), icon, atRuleName, null, null); proposals.add(proposal); } } } return proposals; } /** * Computes the completion proposals for properties. A property may only * appear at the left-hand side of a declaration, so we check whether the * document offset for which the proposals were requested is a legal * position for a property, and only then compute the actual proposals. * * @param document the document * @param offset the offset into the document at which completion was * requested * @param prefix the string immediately before the offset * @return the list of {@link ICompletionProposal}s */ private List proposeProperties(IDocument document, int offset, String prefix) throws BadLocationException { List proposals = new ArrayList(); if (isProperty(document, offset - 1)) { Image propertyIcon = CssUI.getDefault().getImageRegistry().get( CssUI.ICON_PROPERTY); Image shorthandIcon = CssUI.getDefault().getImageRegistry().get( CssUI.ICON_SHORTHAND); Collection propertyNames = profile.getProperties(); for (Iterator i = propertyNames.iterator(); i.hasNext(); ) { String propertyName = (String) i.next(); if (propertyName.startsWith(prefix)) { Image icon = propertyIcon; IPropertyInfo info = profile.getPropertyInfo(propertyName); if (info.isShorthand()) { icon = shorthandIcon; } ICompletionProposal proposal = new CompletionProposal( propertyName, offset - prefix.length(), prefix.length(), propertyName.length(), icon, propertyName, null, info.getDescription()); proposals.add(proposal); } } } return proposals; } private List proposePseudoClasses(IDocument document, int offset, String prefix) throws BadLocationException { List proposals = new ArrayList(); if (isSelector(document, offset - 1)) { Image icon = CssUI.getDefault().getImageRegistry().get( CssUI.ICON_PSEUDO_CLASS); Collection pseudoClassNames = profile.getPseudoClassNames(); for (Iterator i = pseudoClassNames.iterator(); i.hasNext(); ) { String pseudoClassName = (String) i.next(); if (pseudoClassName.startsWith(prefix)) { ICompletionProposal proposal = new CompletionProposal( pseudoClassName, offset - prefix.length(), prefix.length(), pseudoClassName.length(), icon, pseudoClassName, null, null); proposals.add(proposal); } } } return proposals; } private void sortProposals(List proposals) { Collections.sort(proposals, new Comparator() { public int compare(Object o1, Object o2) { String s1 = ((ICompletionProposal) o1).getDisplayString(); if (s1 == null) { return -1; } String s2 = ((ICompletionProposal) o2).getDisplayString(); return s1.compareToIgnoreCase(s2); } }); } }