/*
* 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);
}
});
}
}