/*
* ====================================================================
*
* The ObjectStyle Group Software License, Version 1.0
*
* Copyright (c) 2005 The ObjectStyle Group and individual authors of the
* software. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met: 1.
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer. 2. Redistributions in
* binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other
* materials provided with the distribution. 3. The end-user documentation
* included with the redistribution, if any, must include the following
* acknowlegement: "This product includes software developed by the ObjectStyle
* Group (http://objectstyle.org/)." Alternately, this acknowlegement may
* appear in the software itself, if and wherever such third-party
* acknowlegements normally appear. 4. The names "ObjectStyle Group" and
* "Cayenne" must not be used to endorse or promote products derived from this
* software without prior written permission. For written permission, please
* contact andrus@objectstyle.org. 5. Products derived from this software may
* not be called "ObjectStyle" nor may "ObjectStyle" appear in their names
* without prior written permission of the ObjectStyle Group.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* OBJECTSTYLE GROUP OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many individuals
* on behalf of the ObjectStyle Group. For more information on the ObjectStyle
* Group, please see <http://objectstyle.org/> .
*
*/
package org.objectstyle.wolips.wodclipse.editor;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
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.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.jface.text.rules.IRule;
import org.eclipse.jface.text.rules.WhitespaceRule;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.part.FileEditorInput;
import org.objectstyle.wolips.bindings.api.ApiUtils;
import org.objectstyle.wolips.bindings.utils.BindingReflectionUtils;
import org.objectstyle.wolips.bindings.wod.TypeCache;
import org.objectstyle.wolips.wodclipse.WodclipsePlugin;
import org.objectstyle.wolips.wodclipse.core.completion.WodCompletionProposal;
import org.objectstyle.wolips.wodclipse.core.completion.WodCompletionUtils;
import org.objectstyle.wolips.wodclipse.core.completion.WodParserCache;
import org.objectstyle.wolips.wodclipse.core.parser.AssignmentOperatorWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.BindingNameRule;
import org.objectstyle.wolips.wodclipse.core.parser.BindingValueRule;
import org.objectstyle.wolips.wodclipse.core.parser.CloseDefinitionWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.ElementNameRule;
import org.objectstyle.wolips.wodclipse.core.parser.ElementTypeOperatorWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.ElementTypeRule;
import org.objectstyle.wolips.wodclipse.core.parser.EndAssignmentWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.OpenDefinitionWordDetector;
import org.objectstyle.wolips.wodclipse.core.parser.OperatorRule;
import org.objectstyle.wolips.wodclipse.core.parser.RulePosition;
import org.objectstyle.wolips.wodclipse.core.parser.WodScanner;
import org.objectstyle.wolips.wodclipse.core.preferences.PreferenceConstants;
/**
* @author mike
*/
public class WodCompletionProcessor implements IContentAssistProcessor {
private WodEditor _editor;
public WodCompletionProcessor(WodEditor editor) {
_editor = editor;
}
public char[] getContextInformationAutoActivationCharacters() {
return null;
}
public IContextInformationValidator getContextInformationValidator() {
return null;
}
public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
return null;
}
public char[] getCompletionProposalAutoActivationCharacters() {
return new char[] { ':', '.', '=' };
}
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int _offset) {
Set<WodCompletionProposal> completionProposalsSet = new TreeSet<WodCompletionProposal>();
try {
int offset = _offset;
TypeCache typeCache = WodParserCache.getTypeCache();
IDocument document = viewer.getDocument();
IEditorInput input = _editor.getEditorInput();
if (input instanceof IPathEditorInput) {
IPathEditorInput pathInput = (IPathEditorInput) input;
IPath path = pathInput.getPath();
IFile wodFile = ResourcesPlugin.getWorkspace().getRoot().getFileForLocation(path);
IProject project = wodFile.getProject();
IJavaProject javaProject = JavaCore.create(project);
// Without an underlying model, we have to rescan the line to
// figure out exactly
// what the current matching rule was, so we know what kind of
// token we're dealing with.
IRegion lineRegion = document.getLineInformationOfOffset(offset);
WodScanner scanner = WodScanner.newWODScanner();
scanner.setRange(document, lineRegion.getOffset(), lineRegion.getLength());
boolean foundToken = false;
RulePosition rulePosition = null;
while (!foundToken && (rulePosition = scanner.nextRulePosition()) != null) {
int tokenOffset = rulePosition.getTokenOffset();
if (offset == lineRegion.getOffset() && offset == tokenOffset) {
foundToken = true;
} else if (offset > tokenOffset && offset <= rulePosition.getTokenEndOffset()) {
foundToken = true;
}
}
// We can't reliably use rulePosition here because it might be
// null ...
int tokenOffset = scanner.getTokenOffset();
int tokenLength = scanner.getTokenLength();
IRule rule = (rulePosition == null) ? null : rulePosition.getRule();
// If you make a completion request in the middle of whitespace,
// we don't want to select the whitespace, so zero out the
// whitespace token offsets.
if (rule instanceof WhitespaceRule) {
int partialOffset = (offset - tokenOffset);
offset += partialOffset;
tokenOffset += partialOffset;
tokenLength = 0;
} else {
viewer.setSelectedRange(offset, tokenLength - (offset - tokenOffset));
}
String token = document.get(tokenOffset, tokenLength);
String tokenType = null;
if (foundToken && rulePosition != null) {
if (rulePosition.isRuleOfType(ElementNameRule.class)) {
tokenType = PreferenceConstants.ELEMENT_NAME;
} else if (rulePosition.isRuleOfType(ElementTypeRule.class)) {
tokenType = PreferenceConstants.ELEMENT_TYPE;
} else if (rulePosition.isRuleOfType(BindingNameRule.class)) {
tokenType = PreferenceConstants.BINDING_NAME;
} else if (rulePosition.isRuleOfType(BindingValueRule.class)) {
tokenType = PreferenceConstants.BINDING_VALUE;
} else if (rulePosition.isRuleOfType(OperatorRule.class)) {
tokenOffset += tokenLength;
tokenLength = 0;
if (RulePosition.isOperatorOfType(rulePosition, CloseDefinitionWordDetector.class)) {
tokenType = PreferenceConstants.ELEMENT_NAME;
} else if (RulePosition.isOperatorOfType(rulePosition, ElementTypeOperatorWordDetector.class)) {
tokenType = PreferenceConstants.ELEMENT_TYPE;
} else if (RulePosition.isOperatorOfType(rulePosition, OpenDefinitionWordDetector.class) || RulePosition.isOperatorOfType(rulePosition, EndAssignmentWordDetector.class)) {
tokenType = PreferenceConstants.BINDING_NAME;
} else if (RulePosition.isOperatorOfType(rulePosition, AssignmentOperatorWordDetector.class)) {
tokenType = PreferenceConstants.BINDING_VALUE;
}
}
}
boolean guessed = false;
// If there was no matching token type, then that means we're
// in an invalid parse state and we have to "guess". So we
// backscan
// until we find an operator that we know about, which will tell
// us
// where we are in the document.
if (tokenType == null) {
int startOffset = tokenOffset;
if (startOffset != 0 && startOffset == document.getLength()) {
startOffset--;
}
// int hintChar = -1;
// for (int startOffset = tokenOffset - 1; tokenType == null
// && startOffset > 0; startOffset--) {
boolean tentativeElementType = false;
for (; tokenType == null && startOffset > 0; startOffset--) {
int ch = document.getChar(startOffset);
if (ch == ':') {
tentativeElementType = true;
//tokenType = PreferenceConstants.ELEMENT_TYPE;
//guessed = true;
} else if (ch == '{' || ch == ';') {
tokenType = PreferenceConstants.BINDING_NAME;
guessed = true;
} else if (ch == '=') {
tokenType = PreferenceConstants.BINDING_VALUE;
guessed = true;
} else if (ch == '}') {
// just being explicit
tokenType = PreferenceConstants.ELEMENT_NAME;
guessed = true;
}
}
if (tentativeElementType && tokenType == PreferenceConstants.BINDING_VALUE) {
tokenType = PreferenceConstants.BINDING_VALUE_NAMESPACE;
guessed = true;
}
else if (tentativeElementType) {
tokenType = PreferenceConstants.ELEMENT_TYPE;
guessed = true;
}
}
if (tokenType == null) {
tokenType = PreferenceConstants.ELEMENT_NAME;
guessed = true;
}
// ... Fill in completion proposals based on the token type
if (tokenType == PreferenceConstants.ELEMENT_NAME) {
// We really need something like the AST ... This is a
// pretty expensive
// way to go here. To find element names that have already
// been mapped, we
// reparse the wod file. Lame.
Set<String> alreadyUsedElementNames = WodScanner.getTextForRulesOfType(document, ElementNameRule.class);
WodParserCache wodParserCache = WodParserCache.parser(((FileEditorInput) _editor.getEditorInput()).getFile());
WodCompletionUtils.fillInElementNameCompletionProposals(alreadyUsedElementNames, token, tokenOffset, offset, completionProposalsSet, guessed, wodParserCache.getHtmlEntry().getHtmlElementCache());
} else if (tokenType == PreferenceConstants.ELEMENT_TYPE) {
WodCompletionUtils.fillInElementTypeCompletionProposals(javaProject, token, tokenOffset, offset, completionProposalsSet, guessed, null);
} else if (tokenType == PreferenceConstants.BINDING_NAME) {
IType elementType = findNearestElementType(javaProject, document, scanner, tokenOffset, typeCache);
WodCompletionUtils.fillInBindingNameCompletionProposals(javaProject, elementType, token, tokenOffset, offset, completionProposalsSet, guessed, typeCache);
} else if (tokenType == PreferenceConstants.BINDING_VALUE) {
// MS: We've probably already found the type for our current component. Just get the parser cache entry
// and pull the type off of it.
IType elementType = _editor.getComponentsLocateResults().getDotJavaType();
boolean checkBindingValue = false;
if (elementType != null) {
checkBindingValue = WodCompletionUtils.fillInBindingValueCompletionProposals(javaProject, elementType, token, tokenOffset, offset, completionProposalsSet, typeCache);
}
if (checkBindingValue) {
try {
// We might (probably do) have a syntactically
// invalid wod file at this point, so we need to
// hunt for the name of the binding that this value
// corresponds to ...
int equalsIndex = WodCompletionProcessor.scanBackFor(document, offset, new char[] { '=' }, false);
int noSpaceIndex = WodCompletionProcessor.scanBackFor(document, equalsIndex - 1, new char[] { ' ', '\t', '\n', '\r' }, true);
int spaceIndex = WodCompletionProcessor.scanBackFor(document, noSpaceIndex, new char[] { ' ', '\t', '\n', '\r' }, false);
String bindingName = document.get(spaceIndex + 1, noSpaceIndex - spaceIndex);
elementType = findNearestElementType(javaProject, document, scanner, offset, typeCache);
String[] validValues = ApiUtils.getValidValues(token, javaProject, _editor.getComponentsLocateResults().getDotJavaType(), elementType, bindingName, typeCache);
if (validValues != null) {
String partialToken = WodCompletionUtils.partialToken(token, tokenOffset, offset);
String lowercasePartialToken = partialToken.toLowerCase();
for (int i = 0; i < validValues.length; i++) {
if (validValues[i].toLowerCase().startsWith(lowercasePartialToken)) {
completionProposalsSet.add(new WodCompletionProposal(token, tokenOffset, offset, validValues[i]));
}
}
}
} catch (Throwable t) {
t.printStackTrace();
WodclipsePlugin.getDefault().log(t);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
WodclipsePlugin.getDefault().log(e);
}
return completionProposalsSet.toArray(new WodCompletionProposal[completionProposalsSet.size()]);
}
public String getErrorMessage() {
return null;
}
protected IType findNearestElementType(IJavaProject _project, IDocument _document, WodScanner _scanner, int _offset, TypeCache cache) throws BadLocationException, JavaModelException {
// Go hunting for the element type in a potentially malformed document
// ...
IType type = null;
int offset = _offset;
while (offset != -1 && type == null) {
offset = WodCompletionProcessor.scanBackFor(_document, offset, new char[] { '{' }, false, true);
int colonOffset = WodCompletionProcessor.scanBackFor(_document, offset, new char[] { ':' }, false, true);
if (colonOffset != -1) {
_scanner.setRange(_document, colonOffset, _offset - colonOffset);
RulePosition elementRulePosition = _scanner.getFirstRulePositionOfType(ElementTypeRule.class);
if (elementRulePosition != null) {
String elementTypeName = elementRulePosition.getText();
type = BindingReflectionUtils.findElementType(_project, elementTypeName, false, cache);
offset = -1;
} else {
// we didn't find a ElementTypeRule
offset--;
}
} else {
// failed colonoscopy
type = null;
}
}
return type;
}
protected static int scanBackFor(IDocument _document, int _offset, char[] _lookForChars, boolean _negate) throws BadLocationException {
return scanBackFor(_document, _offset, _lookForChars, _negate, false);
}
protected static int scanBackFor(IDocument _document, int _offset, char[] _lookForChars, boolean _negate, boolean excludeQuoted) throws BadLocationException {
int offset = _offset;
if (offset >= _document.getLength()) {
offset--;
}
int foundIndex = -1;
boolean inQuote = false;
char quoteChar = 0;
for (int i = offset; foundIndex == -1 && i >= 0; i--) {
char ch = _document.getChar(i);
if (ch == '\'' || ch == '"') {
boolean escaped = i > 0 && _document.getChar(i-1) == '\\';
if (inQuote && quoteChar == ch && !escaped) {
inQuote = false;
quoteChar = 0;
}
else if (!inQuote){
inQuote = true;
quoteChar = ch;
}
}
for (int lookForCharNum = 0; foundIndex == -1 && lookForCharNum < _lookForChars.length; lookForCharNum++) {
if (ch == _lookForChars[lookForCharNum] && !(inQuote && excludeQuoted)) {
foundIndex = i;
}
}
if (_negate) {
if (foundIndex != -1) {
foundIndex = -1;
} else {
foundIndex = i;
}
}
}
return foundIndex;
}
}