intial source from ttp://www.sf.net/projects/wdte
[phpeclipse.git] / archive / net.sourceforge.phpeclipse.css.ui / src / net / sourceforge / phpeclipse / css / ui / internal / text / CssContentAssistProcessor.java
1 /*
2  * Copyright (c) 2003-2004 Christopher Lenz and others.
3  * All rights reserved. This program and the accompanying materials 
4  * are made available under the terms of the Common Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/cpl-v10.html
7  * 
8  * Contributors:
9  *     Christopher Lenz - initial API and implementation
10  * 
11  * $Id: CssContentAssistProcessor.java,v 1.1 2004-09-02 18:11:48 jsurfer Exp $
12  */
13
14 package net.sourceforge.phpeclipse.css.ui.internal.text;
15
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.Comparator;
20 import java.util.Iterator;
21 import java.util.List;
22
23 import net.sourceforge.phpeclipse.core.model.ISourceReference;
24 import net.sourceforge.phpeclipse.css.core.internal.text.CssTextUtils;
25 import net.sourceforge.phpeclipse.css.core.model.IAtRule;
26 import net.sourceforge.phpeclipse.css.core.model.IDeclaration;
27 import net.sourceforge.phpeclipse.css.core.model.IPropertyInfo;
28 import net.sourceforge.phpeclipse.css.core.model.IRule;
29 import net.sourceforge.phpeclipse.css.core.model.IStyleRule;
30 import net.sourceforge.phpeclipse.css.core.model.IStyleSheet;
31 import net.sourceforge.phpeclipse.css.core.profiles.IProfile;
32 import net.sourceforge.phpeclipse.css.ui.CssUI;
33 import net.sourceforge.phpeclipse.css.ui.internal.CssDocumentProvider;
34 import net.sourceforge.phpeclipse.css.ui.internal.CssUIPreferences;
35
36 import org.eclipse.jface.preference.IPreferenceStore;
37 import org.eclipse.jface.text.BadLocationException;
38 import org.eclipse.jface.text.IDocument;
39 import org.eclipse.jface.text.IRegion;
40 import org.eclipse.jface.text.ITextViewer;
41 import org.eclipse.jface.text.contentassist.CompletionProposal;
42 import org.eclipse.jface.text.contentassist.ICompletionProposal;
43 import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
44 import org.eclipse.jface.text.contentassist.IContextInformation;
45 import org.eclipse.jface.text.contentassist.IContextInformationValidator;
46 import org.eclipse.swt.graphics.Image;
47 import org.eclipse.ui.texteditor.IDocumentProvider;
48 import org.eclipse.ui.texteditor.ITextEditor;
49
50 /**
51  * Content assist processor for the CSS editor.
52  * 
53  * TODO If a completion proposal is requested before the style sheet has been
54  *      reconciled, we might not get any proposals, or they might be false.
55  */
56 public class CssContentAssistProcessor implements IContentAssistProcessor {
57
58         // Constants ---------------------------------------------------------------
59
60         private static final String AUTOACTIVATION_TRIGGERS =
61                 CssUIPreferences.CONTENTASSIST_AUTOACTIVATION_TRIGGERS;
62
63         private static final String ORDER_PROPOSALS =
64                 CssUIPreferences.CONTENTASSIST_ORDER_PROPOSALS;
65
66         // Instance Variables ------------------------------------------------------
67
68         /**
69          * The preference store.
70          */
71         private IPreferenceStore store;
72
73         /**
74          * The current CSS profile.
75          */
76         private IProfile profile;
77
78         /**
79          * The associated text editor, if any.
80          */
81         private ITextEditor editor;
82
83         // Constructors ------------------------------------------------------------
84
85         /**
86          * Constructor.
87          * 
88          * @param profile The CSS profile to use
89          */
90         public CssContentAssistProcessor(IPreferenceStore store, IProfile profile,
91                 ITextEditor editor) {
92                 this.store = store;
93                 this.profile = profile;
94                 this.editor = editor;
95         }
96
97         // IContentAssistProcessor -------------------------------------------------
98
99         /*
100          * @see IContentAssistProcessor#computeCompletionProposals
101          */
102         public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer,
103                 int documentOffset) {
104                 List retVal = Collections.EMPTY_LIST;
105                 IDocument document = viewer.getDocument();
106                 try {
107                         String prefix = getPrefix(document, documentOffset);
108                         if (prefix.startsWith("@")) { //$NON-NLS-1$
109                                 retVal = proposeAtKeywords(document, documentOffset,
110                                         prefix.substring(1));
111                         } else if (prefix.startsWith(":")) { //$NON-NLS-1$
112                                 retVal = proposePseudoClasses(document, documentOffset,
113                                         prefix.substring(1));
114                         } else {
115                                 retVal = proposeProperties(document, documentOffset, prefix);
116                         }
117                         if ((store != null) && store.getBoolean(ORDER_PROPOSALS)) {
118                                 sortProposals(retVal);
119                         }
120                 } catch (BadLocationException e) {
121                         CssUI.log(
122                                 "Unable to compute completion proposals", e); //$NON-NLS-1$
123                 }
124                 return (ICompletionProposal[]) retVal.toArray(
125                         new ICompletionProposal[retVal.size()]);
126         }
127
128         /*
129          * @see IContentAssistProcessor#computeContextInformation
130          */
131         public IContextInformation[] computeContextInformation(ITextViewer viewer,
132                 int documentOffset) {
133                 return new IContextInformation[0];
134         }
135
136         /*
137          * @see IContentAssistProcessor#getCompletionProposalAutoActivationCharacters
138          */
139         public char[] getCompletionProposalAutoActivationCharacters() {
140                 String chars = store.getString(AUTOACTIVATION_TRIGGERS);
141                 if (chars == null) {
142                         return null;
143                 }
144                 return chars.toCharArray();
145         }
146
147         /*
148          * @see IContentAssistProcessor#getContextInformationAutoActivationCharacters
149          */
150         public char[] getContextInformationAutoActivationCharacters() {
151                 return null;
152         }
153
154         /*
155          * @see IContentAssistProcessor#getErrorMessage
156          */
157         public String getErrorMessage() {
158                 return null;
159         }
160
161         /*
162          * @see IContentAssistProcessor#getContextInformationValidator
163          */
164         public IContextInformationValidator getContextInformationValidator() {
165                 return null;
166         }
167
168         // Private Methods ---------------------------------------------------------
169
170         /**
171          * Returns the part of the word immediately before the position at which
172          * content assist was requested. The prefix is lower-cased before it is 
173          * returned, to enable case-insensitive matching.
174          * 
175          * @param document the document
176          * @param offset the offset into the document
177          * @return the prefix
178          */
179         private String getPrefix(IDocument document, int offset) {
180                 try {
181                         int startPos = offset;
182                         while (startPos > 0) {
183                                 char c = document.getChar(startPos - 1);
184                                 if (!CssTextUtils.isCssIdentifierPart(c)
185                                  && (c != '@') && (c != ':')) {
186                                         break;
187                                 }
188                                 startPos--;
189                                 if ((c == '@') || (c == ':')) {
190                                         break;
191                                 }
192                         }
193                         if (startPos < offset) {
194                                 return document.get(startPos, offset - startPos).toLowerCase();
195                         }
196                 } catch (BadLocationException e) {
197                         e.printStackTrace();
198                 }
199                 return ""; //$NON-NLS-1$
200         }
201
202         /**
203          * Returns the parsed model of the document loaded in the editor, or
204          * <code>null</code> if the editor hasn't been set or the model couldn't be
205          * retrieved.
206          * 
207          * @return the parsed model
208          */
209         private IStyleSheet getStyleSheet() {
210                 if (editor != null) {
211                         IDocumentProvider provider = editor.getDocumentProvider();
212                         if (provider instanceof CssDocumentProvider) {
213                                 return ((CssDocumentProvider) provider).getStyleSheet(
214                                         editor.getEditorInput());
215                         }
216                 }
217                 return null;
218         }
219
220         public boolean isAtKeyword(IDocument document, int offset)
221                 throws BadLocationException {
222                 IStyleSheet styleSheet = getStyleSheet();
223                 if (styleSheet != null) {
224                         IRule rule = styleSheet.getRuleAt(offset);
225                         if (rule instanceof IAtRule) {
226                                 ISourceReference name = ((IAtRule) rule).getName();
227                                 if (name != null) {
228                                         IRegion region = name.getSourceRegion();
229                                         for (int j = region.getOffset(); j < offset; j++) {
230                                                 char ch = document.getChar(j);
231                                                 if (!CssTextUtils.isCssIdentifierPart(ch)) {
232                                                         return false;
233                                                 }
234                                         }
235                                         return true;
236                                 }
237                         } else if (rule == null) {
238                                 return true;
239                         }
240                 }
241                 return false;
242         }
243
244         /**
245          * Determines whether the document contains a property at the specified
246          * offset, or at least whether a property is theoretically allowed at that
247          * offset. 
248          * 
249          * @param document the document
250          * @param offset the offset into the document
251          * @return <code>true</code> if the specified offset is a legal position for
252          *         a property, <code>false</code> otherwise
253          * @throws BadLocationException if there was a problem accessing the
254          *         document
255          */
256         public boolean isProperty(IDocument document, int offset)
257                 throws BadLocationException {
258                 IStyleSheet styleSheet = getStyleSheet();
259                 if (styleSheet != null) {
260                         IRule rule = styleSheet.getRuleAt(offset);
261                         if (rule != null) {
262                                 IDeclaration declaration = rule.getDeclarationAt(offset);
263                                 if (declaration != null) {
264                                         IRegion region = declaration.getSourceRegion();
265                                         for (int j = region.getOffset(); j < offset; j++) {
266                                                 if (document.getChar(j) == ':') {
267                                                         return false;
268                                                 }
269                                         }
270                                         return true;
271                                 } else {
272                                         IRegion region = rule.getSourceRegion();
273                                         for (int j = region.getOffset(); j < offset; j++) {
274                                                 if (document.getChar(j) == '{') {
275                                                         return true;
276                                                 }
277                                         }
278                                 }
279                         }
280                 }
281                 return false;
282         }
283
284         /**
285          * Determines whether the document contains a selector at the specified
286          * offset, or at least whether a selector is theoretically allowed at that
287          * offset.
288          */
289         public boolean isSelector(IDocument document, int offset)
290                 throws BadLocationException {
291                 IStyleSheet styleSheet = getStyleSheet();
292                 if (styleSheet != null) {
293                         IRule rule = styleSheet.getRuleAt(offset);
294                         if (rule instanceof IStyleRule) {
295                                 ISourceReference selector = ((IStyleRule) rule).getSelector();
296                                 if (selector != null) {
297                                         IRegion region = selector.getSourceRegion();
298                                         for (int j = region.getOffset(); j < offset; j++) {
299                                                 if (document.getChar(j) == '{') {
300                                                         return false;
301                                                 }
302                                         }
303                                         return true;
304                                 }
305                         } else if (rule == null) {
306                                 return true;
307                         }
308                 }
309                 return false;
310         }
311
312         private List proposeAtKeywords(IDocument document, int offset,
313                 String prefix) throws BadLocationException {
314                 List proposals = new ArrayList();
315                 if (isAtKeyword(document, offset)) {
316                         Image icon = CssUI.getDefault().getImageRegistry().get(
317                                 CssUI.ICON_AT_RULE);
318                         Collection atRuleNames = profile.getAtKeywords();
319                         for (Iterator i = atRuleNames.iterator(); i.hasNext(); ) {
320                                 String atRuleName = (String) i.next();
321                                 if (atRuleName.startsWith(prefix)) {
322                                         ICompletionProposal proposal = new CompletionProposal(
323                                                 atRuleName, offset - prefix.length(), prefix.length(),
324                                                 atRuleName.length(), icon, atRuleName, null, null);
325                                         proposals.add(proposal);
326                                 }
327                         }
328                 }
329                 return proposals;
330         }
331
332         /**
333          * Computes the completion proposals for properties. A property may only
334          * appear at the left-hand side of a declaration, so we check whether the
335          * document offset for which the proposals were requested is a legal
336          * position for a property, and only then compute the actual proposals.
337          * 
338          * @param document the document
339          * @param offset the offset into the document at which completion was
340          *        requested
341          * @param prefix the string immediately before the offset
342          * @return the list of {@link ICompletionProposal}s
343          */
344         private List proposeProperties(IDocument document, int offset,
345                 String prefix) throws BadLocationException {
346                 List proposals = new ArrayList();
347                 if (isProperty(document, offset - 1)) {
348                         Image propertyIcon =
349                                 CssUI.getDefault().getImageRegistry().get(
350                                         CssUI.ICON_PROPERTY);
351                         Image shorthandIcon =
352                                 CssUI.getDefault().getImageRegistry().get(
353                                         CssUI.ICON_SHORTHAND);
354                         Collection propertyNames = profile.getProperties();
355                         for (Iterator i = propertyNames.iterator(); i.hasNext(); ) {
356                                 String propertyName = (String) i.next();
357                                 if (propertyName.startsWith(prefix)) {
358                                         Image icon = propertyIcon;
359                                         IPropertyInfo info = profile.getPropertyInfo(propertyName);
360                                         if (info.isShorthand()) {
361                                                 icon = shorthandIcon;
362                                         }
363                                         ICompletionProposal proposal = new CompletionProposal(
364                                                 propertyName, offset - prefix.length(),
365                                                 prefix.length(), propertyName.length(), icon,
366                                                 propertyName, null, info.getDescription());
367                                         proposals.add(proposal);
368                                 }
369                         }
370                 }
371                 return proposals;
372         }
373
374         private List proposePseudoClasses(IDocument document, int offset,
375                 String prefix) throws BadLocationException {
376                 List proposals = new ArrayList();
377                 if (isSelector(document, offset - 1)) {
378                         Image icon = CssUI.getDefault().getImageRegistry().get(
379                                 CssUI.ICON_PSEUDO_CLASS);
380                         Collection pseudoClassNames = profile.getPseudoClassNames();
381                         for (Iterator i = pseudoClassNames.iterator(); i.hasNext(); ) {
382                                 String pseudoClassName = (String) i.next();
383                                 if (pseudoClassName.startsWith(prefix)) {
384                                         ICompletionProposal proposal = new CompletionProposal(
385                                                 pseudoClassName, offset - prefix.length(),
386                                                 prefix.length(), pseudoClassName.length(), icon,
387                                                 pseudoClassName, null, null);
388                                         proposals.add(proposal);
389                                 }
390                         }
391                 }
392                 return proposals;
393         }
394
395         private void sortProposals(List proposals) {
396                 Collections.sort(proposals, new Comparator() {
397                         public int compare(Object o1, Object o2) {
398                                 String s1 = ((ICompletionProposal) o1).getDisplayString();
399                                 if (s1 == null) {
400                                         return -1;
401                                 }
402                                 String s2 = ((ICompletionProposal) o2).getDisplayString();
403                                 return s1.compareToIgnoreCase(s2);
404                         }
405                 });
406         }
407
408 }