blob: 6550076297fce8b1c33dcf35dd2be791db1d9c74 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2012 Obeo.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.acceleo.internal.ide.ui.editors.template;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.acceleo.ide.ui.AcceleoUIActivator;
import org.eclipse.acceleo.internal.ide.ui.AcceleoUIMessages;
import org.eclipse.acceleo.internal.ide.ui.editors.template.utils.OpenDeclarationUtils;
import org.eclipse.acceleo.parser.cst.CSTNode;
import org.eclipse.acceleo.parser.cst.TypedModel;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.IItemLabelProvider;
import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.ocl.ecore.IteratorExp;
import org.eclipse.ocl.utilities.ASTNode;
import org.eclipse.ui.texteditor.ITextEditor;
/**
* This will allow us to plug the CTRL+click "open declaration" in Acceleo editors.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class AcceleoElementHyperlinkDetector extends AbstractHyperlinkDetector {
/** Adapter factory instance. This contains all factories registered in the global registry. */
private static final ComposedAdapterFactory FACTORY = createAdapterFactory();
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.hyperlink.IHyperlinkDetector#detectHyperlinks(org.eclipse.jface.text.ITextViewer,
* org.eclipse.jface.text.IRegion, boolean)
*/
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region,
boolean canShowMultipleHyperlinks) {
final ITextEditor textEditor = (ITextEditor)getAdapter(ITextEditor.class);
if (region != null && textEditor instanceof AcceleoEditor) {
int offset = region.getOffset();
final AcceleoEditor editor = (AcceleoEditor)textEditor;
final int start = Math.max(0, offset - 10);
final int end = Math.min(editor.getContent().getText().length(), offset + 10);
// Creates a new String to avoid keeping the whole document in memory
final String expressionSurroundings = new String(editor.getContent().getText().substring(start,
end));
if (isRelevant(expressionSurroundings, offset - start)) {
return detectHyperlinks(editor, offset);
}
}
return null;
}
/**
* This will be called from {@link #detectHyperlinks(ITextViewer, IRegion, boolean)} when we've taken care
* of all shortcut routes.
*
* @param editor
* The AcceleoEditor in which to detect hyperlinks.
* @param offset
* Offset at which to search for hyperlinks.
* @return The detected hyperlinks if any.
*/
private IHyperlink[] detectHyperlinks(AcceleoEditor editor, int offset) {
EObject res = null;
int wordStart = -1;
int wordLength = -1;
/*
* This boolean will be used to determine whether we need to compute a smarter region than what's
* carried on by the AST/CST node.
*/
boolean inferWordRegion = true;
ASTNode astNode = editor.getContent().getResolvedASTNode(offset, offset);
if (astNode != null) {
res = OpenDeclarationUtils.findDeclarationFromAST(astNode);
if (res instanceof IteratorExp && editor.getContent().getOCLEnvironment() != null) {
res = OpenDeclarationUtils.findIteratorEOperation(editor.getContent().getOCLEnvironment(),
(IteratorExp)res);
}
wordStart = astNode.getStartPosition();
wordLength = astNode.getEndPosition() - astNode.getStartPosition();
}
if (res == null) {
CSTNode cstNode = editor.getContent().getCSTNode(offset, offset);
if (cstNode != null) {
res = OpenDeclarationUtils.findDeclarationFromCST(editor, astNode, cstNode);
wordStart = cstNode.getStartPosition();
wordLength = cstNode.getEndPosition() - cstNode.getStartPosition();
}
if (cstNode instanceof TypedModel) {
inferWordRegion = false;
}
}
IHyperlink[] links = null;
if (res != null) {
final IRegion wordRegion;
if (inferWordRegion) {
wordRegion = getWordRegion(editor, offset, wordStart, wordLength);
} else {
wordRegion = new Region(wordStart, wordLength);
}
if (wordRegion != null) {
links = new IHyperlink[1];
links[0] = new AcceleoElementHyperlink(editor, wordRegion, res);
}
}
return links;
}
/**
* Tries and return the actual region span of the word under "cursorOffset". Note that a "word" can
* actualy contain spaces, dots, colons, ...
* <p>
* For example, if the cursor is currently on the "metamodel" declaration, then this will return the full
* extent of the metamodel's URI, regardless of slashes, dots, colons, sharps, ...
* </p>
* <p>
* On a type expression, this can and will return the full type if it is qualified, packages included
* (mt::core::Query) and whether or not there are spaces in-between colons and package/type names.
* </p>
* <p>
* On a variable declaration, this will return the region containing both the variable name and its type,
* once again regardless of spaces (templ : mt::core::Template).
* </p>
*
* @param currentEditor
* The editor currently displaying the text we search a region from.
* @param cursorOffset
* Offset above which the mouse currently hovers.
* @param expressionStart
* Starting offset of the expression in which we seek a particular region.
* @param expressionLength
* Ending offset of the expression in which we seek a particular region.
* @return Region of the word we're currently hovering over.
*/
private IRegion getWordRegion(AcceleoEditor currentEditor, int cursorOffset, int expressionStart,
int expressionLength) {
if ((expressionStart + expressionLength) > currentEditor.getContent().getText().length()) {
return null;
}
// Creates a new String to avoid keeping the whole document in memory
final String expression = new String(currentEditor.getContent().getText().substring(expressionStart,
expressionStart + expressionLength));
int cursorPositionInExpression = cursorOffset - expressionStart;
final int wordStart;
final int wordEnd;
int prev = cursorPositionInExpression - 1;
if (prev >= 0) {
while (prev >= 0 && isRelevant(expression, prev)) {
prev--;
}
}
int next = cursorPositionInExpression + 1;
if (next < expression.length()) {
while (next < expression.length() - 1 && isRelevant(expression, next)) {
next++;
}
}
if (prev == -1) {
wordStart = 0;
} else {
// We found a non relevant character at "prev". Add 1 to start at the last browsed.
wordStart = prev + 1;
}
if (next == expression.length() - 1) {
wordEnd = expression.length();
} else {
wordEnd = next;
}
// now set back the offsets in document range
int wordLength = wordEnd - wordStart;
return new Region(wordStart + expressionStart, wordLength);
}
/**
* Given an expression and offset in that expression, this method will check wether the character at
* <em>offset</em> is relevant for hyperlinking. Some examples are given in the javadoc of
* {@link #getWordRegion(AcceleoEditor, int, int, int)}.
*
* @param expression
* Expression in which a character is to be considered.
* @param offset
* Offset of the character we need to determine relevancy of.
* @return <code>true</code> if the character at <em>offset</em> in <em>expression</em> is relevant for
* hyperlink regions.
*/
private boolean isRelevant(String expression, int offset) {
char character = expression.charAt(offset);
// shortcut
if (Character.isJavaIdentifierPart(character)) {
return true;
}
boolean relevant = false;
// initialize at random "relevant" character : doesn't matter which as long as it isn't '-' (arrows)
char previous = 'a';
char next = 'a';
if (offset > 1) {
previous = expression.charAt(offset - 1);
}
if (offset < expression.length() - 2) {
next = expression.charAt(offset + 1);
}
/*
* ':' is somewhat special in that it is relevant and can serve as a "junction" in variable
* declaration and type expressions. In both of these, ':', '::' *and* the spaces before and after are
* relevant.
*/
if (Character.isWhitespace(character)) {
int curOffset = offset;
while (curOffset < expression.length() - 2 && !Character.isJavaIdentifierPart(next)
&& next != ':') {
next = expression.charAt(++curOffset);
}
curOffset = offset;
while (curOffset > 1 && !Character.isJavaIdentifierPart(previous) && previous != ':') {
previous = expression.charAt(--curOffset);
}
if (previous == ':' || next == ':') {
relevant = true;
}
} else if (character == ':') {
relevant = true;
} else if (character == '>' && previous == '-') {
relevant = false;
} else if (character == '-' && next == '>') {
relevant = false;
} else if (character == '<' || character == '>' || character == '=') {
relevant = true;
} else if (character == '+' || character == '-' || character == '/' || character == '*') {
relevant = true;
}
return relevant;
}
/**
* Returns an adapter factory containing all the global EMF registry's factories.
*
* @return An adapter factory made of all the adapter factories declared in the registry.
*/
private static ComposedAdapterFactory createAdapterFactory() {
final List<AdapterFactory> factories = new ArrayList<AdapterFactory>();
factories.add(new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE));
factories.add(new ReflectiveItemProviderAdapterFactory());
return new ComposedAdapterFactory(factories);
}
/**
* This will try and get the IItemLabelProvider associated to the given EObject if its ItemProviderFactory
* is registered, then return the text it provides.
*
* @param eObj
* EObject we need the text of.
* @return The text provided by the IItemLabelProvider associated with <tt>eObj</tt>, <code>null</code> if
* it cannot be found.
* @see IItemLabelProvider#getText(Object)
* @since 0.8
*/
protected static String getLabelFor(EObject eObj) {
final String text;
if (eObj == null) {
text = "null"; //$NON-NLS-1$
} else {
final IItemLabelProvider labelProvider = (IItemLabelProvider)FACTORY.adapt(eObj,
IItemLabelProvider.class);
if (labelProvider != null) {
text = labelProvider.getText(eObj);
} else {
text = ""; //$NON-NLS-1$
}
}
return text;
}
/**
* This implementation of an hyperlink allows for the opening of Acceleo elements declarations.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
private class AcceleoElementHyperlink implements IHyperlink {
/** Region of this hyperlink. */
private final IRegion hyperLinkRegion;
/** EObject that will be opened via this hyperlink. */
private final EObject target;
/** Editor on which this link appears. */
private final ITextEditor sourceEditor;
/**
* Instantiates an Acceleo hyperlink given the editor it appears on, the text region it spans to, and
* the link's target.
*
* @param editor
* Editor on which this hyperlink is shown.
* @param region
* Region of the editor where this hyperlink appears.
* @param linkTarget
* Target of the hyperlink.
*/
public AcceleoElementHyperlink(ITextEditor editor, IRegion region, EObject linkTarget) {
sourceEditor = editor;
hyperLinkRegion = region;
target = linkTarget;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getHyperlinkRegion()
*/
public IRegion getHyperlinkRegion() {
return hyperLinkRegion;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getHyperlinkText()
*/
public String getHyperlinkText() {
return AcceleoUIMessages.getString("AcceleoElementHyperlinkDetector.OpenDeclarationLabel", //$NON-NLS-1$
getLabelFor(target));
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getTypeLabel()
*/
public String getTypeLabel() {
return null;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.jface.text.hyperlink.IHyperlink#open()
*/
public void open() {
if (target.eResource() != null && sourceEditor.getSite() != null
&& sourceEditor.getSite().getPage() != null) {
OpenDeclarationUtils.showEObject(sourceEditor.getSite().getPage(), target.eResource()
.getURI(), OpenDeclarationUtils.createRegion(target), target);
} else {
AcceleoUIActivator.log(AcceleoUIMessages.getString(
"AcceleoElementHyperlinkDetector.MetamodelNotInAResource", target.eClass() //$NON-NLS-1$
.getName()), false);
}
}
}
}