blob: 76c3822325f4594764327d26b49624cd1f5706a8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2003, 2015 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
* Les Jones <lesojones@gmail.com> - Bug 214511
* Benjamin Cabe <benjamin.cabe@anyware-tech.com> - bug 262622
*******************************************************************************/
package org.eclipse.pde.internal.ui.editor.plugin;
import java.util.*;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.viewers.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.pde.core.IBaseModel;
import org.eclipse.pde.core.plugin.IPluginLibrary;
import org.eclipse.pde.core.plugin.IPluginModelBase;
import org.eclipse.pde.internal.core.ibundle.*;
import org.eclipse.pde.internal.core.plugin.ImportObject;
import org.eclipse.pde.internal.core.text.*;
import org.eclipse.pde.internal.core.text.bundle.*;
import org.eclipse.pde.internal.ui.*;
import org.eclipse.pde.internal.ui.editor.*;
import org.eclipse.pde.internal.ui.editor.actions.PDEActionConstants;
import org.eclipse.pde.internal.ui.editor.text.*;
import org.eclipse.pde.internal.ui.elements.DefaultContentProvider;
import org.eclipse.pde.internal.ui.refactoring.PDERefactoringAction;
import org.eclipse.pde.internal.ui.refactoring.RefactoringActionFactory;
import org.eclipse.pde.internal.ui.util.SharedLabelProvider;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.forms.editor.FormEditor;
import org.osgi.framework.Constants;
public class BundleSourcePage extends KeyValueSourcePage {
/**
* Used to set the selection in the outline view with the link with editor
* feature is enabled
* Cannot use a document range object because manifest header elements
* do not have ranges associated with them in the bundle model
*/
private Object fTargetOutlineSelection;
/**
* Offset used to set the current highlight range.
* Used to prevent cyclic event firing when a selection is made in the
* outline view with the link with editor feature on. When a selection
* is made in the source viewer, an event is fired back to the outline
* view to unnecessarily update the selection.
*/
private int fCurrentHighlightRangeOffset;
private final int F_NOT_SET = -1;
private PDERefactoringAction fRenameAction;
/**
* BundleOutlineContentProvider
*
*/
private class BundleOutlineContentProvider extends DefaultContentProvider implements ITreeContentProvider {
@Override
public Object[] getChildren(Object parent) {
// Need an identifying class for label provider
if (parent instanceof ImportPackageHeader) {
return ((ImportPackageHeader) parent).getPackages();
} else if (parent instanceof ExportPackageHeader) {
return ((ExportPackageHeader) parent).getPackages();
} else if (parent instanceof RequiredExecutionEnvironmentHeader) {
return ((RequiredExecutionEnvironmentHeader) parent).getEnvironments();
} else if (parent instanceof RequireBundleHeader) {
return ((RequireBundleHeader) parent).getRequiredBundles();
} else if (parent instanceof BundleClasspathHeader) {
return getPluginLibraries();
}
return new Object[0];
}
private Object[] getPluginLibraries() {
IPluginLibrary[] libraries = getBundleClasspathLibraries();
if ((libraries == null) || (libraries.length == 0)) {
return new Object[0];
}
return libraries;
}
@Override
public boolean hasChildren(Object parent) {
return getChildren(parent).length > 0;
}
@Override
public Object getParent(Object child) {
return null;
}
@Override
public Object[] getElements(Object parent) {
if (parent instanceof BundleModel) {
BundleModel model = (BundleModel) parent;
Map<?, ?> manifest = ((Bundle) model.getBundle()).getHeaders();
ArrayList<IDocumentKey> keys = new ArrayList<>();
for (Iterator<?> elements = manifest.keySet().iterator(); elements.hasNext();) {
IDocumentKey key = (IDocumentKey) manifest.get(elements.next());
if (key.getOffset() > -1)
keys.add(key);
}
return keys.toArray();
}
return new Object[0];
}
}
private IPluginLibrary[] getBundleClasspathLibraries() {
// The bundle classpath header has no model data members
// Retrieve the plug-in library equivalents from the editor model
FormEditor editor = getEditor();
if (editor instanceof PDEFormEditor) {
PDEFormEditor formEditor = (PDEFormEditor) editor;
IBaseModel baseModel = formEditor.getAggregateModel();
if (baseModel instanceof IPluginModelBase) {
IPluginLibrary[] libraries = ((IPluginModelBase) baseModel).getPluginBase().getLibraries();
return libraries;
}
}
return null;
}
private class BundleLabelProvider extends LabelProvider {
// TODO: MP: QO: LOW: Move to PDELabelProvider
@Override
public String getText(Object obj) {
if (obj instanceof PackageObject) {
return ((PackageObject) obj).getName();
} else if (obj instanceof ExecutionEnvironment) {
return ((ExecutionEnvironment) obj).getName();
} else if (obj instanceof RequireBundleObject) {
return getTextRequireBundle(((RequireBundleObject) obj));
} else if (obj instanceof ManifestHeader) {
return ((ManifestHeader) obj).getName();
}
return super.getText(obj);
}
private String getTextRequireBundle(RequireBundleObject bundle) {
StringBuffer label = new StringBuffer();
// Append the ID
label.append(bundle.getId());
// Get the version
String version = bundle.getVersion();
// If there is no version, just return what we have
if ((version == null) || (version.length() == 0)) {
return label.toString();
}
// Append a space
label.append(' ');
// If the first character does not have a range indicator,
// add a default one. This can happen when there is only one
// value specified for either min or max
char firstChar = version.charAt(0);
if ((firstChar != '(') && (firstChar != '[')) {
label.append('(');
}
// Append the version
label.append(version);
// If the last character does not have a range indicator,
// add a default one. This can happen when there is only one
// value specified for either min or max
char lastChar = version.charAt(version.length() - 1);
if ((lastChar != ')') && (lastChar != ']')) {
label.append(')');
}
// Return what we have
return label.toString();
}
@Override
public Image getImage(Object obj) {
PDELabelProvider labelProvider = PDEPlugin.getDefault().getLabelProvider();
if (obj instanceof PackageObject) {
return labelProvider.get(PDEPluginImages.DESC_PACKAGE_OBJ);
} else if (obj instanceof ExecutionEnvironment) {
return labelProvider.get(PDEPluginImages.DESC_JAVA_LIB_OBJ);
} else if (obj instanceof RequireBundleObject) {
int flags = SharedLabelProvider.F_EXTERNAL;
if (((RequireBundleObject) obj).isReexported()) {
flags = flags | SharedLabelProvider.F_EXPORT;
}
return labelProvider.get(PDEPluginImages.DESC_REQ_PLUGIN_OBJ, flags);
} else if (obj instanceof ManifestHeader) {
return labelProvider.get(PDEPluginImages.DESC_BUILD_VAR_OBJ);
} else if (obj instanceof IPluginLibrary) {
return labelProvider.get(PDEPluginImages.DESC_JAVA_LIB_OBJ);
}
return null;
}
}
/**
* @param editor
* @param id
* @param title
*/
public BundleSourcePage(PDEFormEditor editor, String id, String title) {
super(editor, id, title);
resetTargetOutlineSelection();
resetCurrentHighlightRangeOffset();
}
/**
* @param offset
*/
private void setCurrentHighlightRangeOffset(int offset) {
fCurrentHighlightRangeOffset = offset;
}
/**
*
*/
private void resetCurrentHighlightRangeOffset() {
fCurrentHighlightRangeOffset = F_NOT_SET;
}
private int getCurrentHighlightRangeOffset() {
return fCurrentHighlightRangeOffset;
}
@Override
public void resetHighlightRange() {
resetCurrentHighlightRangeOffset();
super.resetHighlightRange();
}
/**
*
*/
private void resetTargetOutlineSelection() {
fTargetOutlineSelection = null;
}
/**
* @param object
*/
private void setTargetOutlineSelection(Object object) {
fTargetOutlineSelection = object;
}
private Object getTargetOutlineSelection() {
return fTargetOutlineSelection;
}
@Override
public ILabelProvider createOutlineLabelProvider() {
return new BundleLabelProvider();
}
@Override
public ITreeContentProvider createOutlineContentProvider() {
return new BundleOutlineContentProvider();
}
@Override
public IDocumentRange getRangeElement(int offset, boolean searchChildren) {
IBundleModel model = (IBundleModel) getInputContext().getModel();
Map<?, ?> manifest = ((Bundle) model.getBundle()).getHeaders();
// Reset
resetTargetOutlineSelection();
// Search each manifest header
for (Iterator<?> elements = manifest.values().iterator(); elements.hasNext();) {
IDocumentRange node = (IDocumentRange) elements.next();
// Check to see if the parent is within range
if (isWithinCurrentRange(offset, node)) {
// Search the children of composite manifest headers first if
// specified
if (searchChildren && (node instanceof CompositeManifestHeader)) {
IDocumentRange child_node = getRangeElementChild(model, offset, (CompositeManifestHeader) node);
// If the child node is specified return it; otherwise, we
// will default to the parent node
if (child_node != null) {
return child_node;
}
}
// A manifest header object can be used both for setting the
// highlight range and making a selection in the outline view
setTargetOutlineSelection(node);
return node;
}
}
return null;
}
/**
* @param offset
* @param range
* @return true if the offset is within the range; false, otherwise
*/
private boolean isWithinCurrentRange(int offset, IDocumentRange range) {
if (range == null) {
// Range not set
return false;
} else if (offset >= range.getOffset() && (offset <= (range.getOffset() + range.getLength()))) {
// Offset within range
return true;
}
// Offset not within range
return false;
}
/**
* This method is required because the calculated ranges, do NOT include
* their parameters (e.g. x-friends, bundle-version)
* @param offset
* @param current_range
* @param previous_range
* @return true if the offset falls in between the end of the previous range
* and before the current range (e.g. the previous ranges parameters)
*/
private boolean isWithinPreviousRange(int offset, IDocumentRange current_range, IDocumentRange previous_range) {
if ((current_range == null) || (previous_range == null)) {
// Range not set
return false;
} else if ((offset >= previous_range.getOffset() + previous_range.getLength()) && ((offset <= current_range.getOffset()))) {
// Offset within range
return true;
}
// Offset not within range
return false;
}
/**
* @param offset
* @param previousRange
*/
private boolean isBeforePreviousRange(int offset, IDocumentRange previousRange) {
if (previousRange == null) {
return false;
} else if (offset < previousRange.getOffset()) {
return true;
}
return false;
}
/**
* @param model
* @param offset
* @param header
*/
private IDocumentRange getRangeElementChild(IBundleModel model, int offset, CompositeManifestHeader header) {
// Ensure the header has associated elements
if (header.isEmpty()) {
return null;
}
// Get the header elements
PDEManifestElement[] elements = header.getElements();
// Get the header elements name (assume that all elements are the same
// as the first element)
String headerName = getHeaderName(elements[0]);
PDEManifestElement previousElement = null;
PDEManifestElement currentElement = null;
IDocumentRange previousRange = null;
IDocumentRange currentRange = null;
// Process each element
for (int i = 0; i < elements.length; i++) {
currentElement = elements[i];
// Find the range for the element
currentRange = getSpecificRange(model, headerName, currentElement.getValue());
// Determine whether the element is within range
if (isBeforePreviousRange(offset, previousRange)) {
return null;
} else if (isWithinCurrentRange(offset, currentRange)) {
setChildTargetOutlineSelection(headerName, currentElement);
// Use for setting the highlight range
return currentRange;
} else if (isWithinPreviousRange(offset, currentRange, previousRange)) {
setChildTargetOutlineSelection(headerName, previousElement);
// Use for setting the highlight range
return previousRange;
}
// Update for the next iteration
previousRange = currentRange;
previousElement = currentElement;
}
if (isWithinLastElementParamRange(offset, currentRange, header)) {
// No element found within range
setChildTargetOutlineSelection(headerName, currentElement);
// Use for setting the highlight range
return currentRange;
}
return null;
}
/**
* @param offset
* @param currentRange
* @param headerRange
*/
private boolean isWithinLastElementParamRange(int offset, IDocumentRange currentRange, IDocumentRange headerRange) {
if (currentRange == null) {
return false;
} else if ((offset >= currentRange.getOffset() + currentRange.getLength()) && (offset <= (headerRange.getOffset() + headerRange.getLength()))) {
return true;
}
return false;
}
/**
* @param headerName
* @param element
*/
private void setChildTargetOutlineSelection(String headerName, PDEManifestElement element) {
// Use for setting the outline view selection
if (headerName.equalsIgnoreCase(Constants.BUNDLE_CLASSPATH)) {
setTargetOutlineSelection(getBundleClasspathOutlineSelection(element));
} else {
setTargetOutlineSelection(element);
}
}
/**
* Edge Case: Cannot use the PDEManifestElement directly to select bundle
* classpath elements in the outline view. Need to use IPluginLibrary
* objects
* @param manifestElement
*/
private Object getBundleClasspathOutlineSelection(PDEManifestElement manifestElement) {
IPluginLibrary[] libraries = getBundleClasspathLibraries();
// Ensure there are libraries
if ((libraries == null) || (libraries.length == 0)) {
return null;
}
// Linearly search for the equivalent library object
for (int i = 0; i < libraries.length; i++) {
if (manifestElement.getValue().equals(libraries[i].getName())) {
// Found
return libraries[i];
}
}
// None found
return null;
}
/**
* @param element
*/
private String getHeaderName(PDEManifestElement element) {
if (element instanceof ExportPackageObject) {
return Constants.EXPORT_PACKAGE;
} else if (element instanceof ImportPackageObject) {
return Constants.IMPORT_PACKAGE;
} else if (element instanceof ExecutionEnvironment) {
return Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT;
} else if (element instanceof RequireBundleObject) {
return Constants.REQUIRE_BUNDLE;
} else {
// Bundle classpath elements do not have their own model class
// They are created as PDEManifestElements directly whose value
// is just a String
// Assume that if the element is none of the above types it is this
// type
return Constants.BUNDLE_CLASSPATH;
}
}
@Override
protected String[] collectContextMenuPreferencePages() {
String[] ids = super.collectContextMenuPreferencePages();
String[] more = new String[ids.length + 1];
more[0] = "org.eclipse.pde.ui.EditorPreferencePage"; //$NON-NLS-1$
System.arraycopy(ids, 0, more, 1, ids.length);
return more;
}
@Override
public IDocumentRange findRange() {
Object selection = getSelection();
if (selection instanceof ImportObject) {
IPluginModelBase base = ((ImportObject) selection).getImport().getPluginModel();
if (base instanceof IBundlePluginModelBase)
return getSpecificRange(((IBundlePluginModelBase) base).getBundleModel(), Constants.REQUIRE_BUNDLE, ((ImportObject) selection).getId());
} else if (selection instanceof ImportPackageObject) {
return getSpecificRange(((ImportPackageObject) selection).getModel(), Constants.IMPORT_PACKAGE, ((ImportPackageObject) selection).getValue());
} else if (selection instanceof ExportPackageObject) {
return getSpecificRange(((ExportPackageObject) selection).getModel(), Constants.EXPORT_PACKAGE, ((ExportPackageObject) selection).getValue());
} else if (selection instanceof IPluginLibrary) {
IPluginModelBase base = ((IPluginLibrary) selection).getPluginModel();
if (base instanceof IBundlePluginModelBase)
return getSpecificRange(((IBundlePluginModelBase) base).getBundleModel(), Constants.BUNDLE_CLASSPATH, ((IPluginLibrary) selection).getName());
} else if (selection instanceof ExecutionEnvironment) {
return getSpecificRange(((ExecutionEnvironment) selection).getModel(), Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT, ((ExecutionEnvironment) selection).getValue());
} else if (selection instanceof RequireBundleObject) {
return getSpecificRange(((RequireBundleObject) selection).getModel(), Constants.REQUIRE_BUNDLE, ((RequireBundleObject) selection).getId());
}
return null;
}
public static IDocumentRange getSpecificRange(IBundleModel model, IManifestHeader header, String element) {
if (header == null || !(model instanceof IEditingModel))
return null;
final int[] range = new int[] {-1, -1}; // { offset, length }
try {
int start = header.getOffset() + header.getName().length();
int length = header.getLength() - header.getName().length();
String headerValue = ((IEditingModel) model).getDocument().get(start, length);
int i = headerValue.indexOf(element);
int last = headerValue.lastIndexOf(element);
if (i > 0 && i != last) {
char[] sChar = element.toCharArray();
char[] headerChar = headerValue.toCharArray();
headLoop: for (; i <= last; i++) {
// check 1st, middle and last chars to speed things up
if (headerChar[i] != sChar[0] && headerChar[i + sChar.length / 2] != sChar[sChar.length / 2] && headerChar[i + sChar.length - 1] != sChar[sChar.length - 1])
continue headLoop;
for (int j = 1; j < sChar.length - 1; j++)
if (headerChar[i + j] != sChar[j])
continue headLoop;
// found match
char c = headerChar[i - 1];
if (!Character.isWhitespace(c) && c != ',')
// search string is contained by another
continue headLoop;
int index = i + sChar.length;
if (index >= headerChar.length) {
// Current match is longer than search
// Occurs when match is '.' or a single character
continue;
}
c = headerChar[index];
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.')
// current match is longer than search
continue headLoop;
break;
}
}
if (i != -1) {
range[0] = start + i;
range[1] = element.length();
}
} catch (BadLocationException e) {
}
if (range[0] == -1) { // if un-set offset use header range
range[0] = header.getOffset();
// Only select the length of the header name; otherwise, the
// header value will be included in the selection
range[1] = header.getName().length();
}
return new IDocumentRange() {
@Override
public int getOffset() {
return range[0];
}
@Override
public int getLength() {
return range[1];
}
};
}
public static IDocumentRange getSpecificRange(IBundleModel model, String headerName, String search) {
IManifestHeader header = model.getBundle().getManifestHeader(headerName);
return getSpecificRange(model, header, search);
}
@Override
protected boolean isSelectionListener() {
return true;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getAdapter(Class<T> adapter) {
if (IHyperlinkDetector.class.equals(adapter))
return (T) new BundleHyperlinkDetector(this);
return super.getAdapter(adapter);
}
@Override
public void updateSelection(Object object) {
// Update the global selection
setSelectedObject(object);
// Highlight the selection if it is a manifest header
if (object instanceof IDocumentKey) {
setHighlightRange((IDocumentKey) object);
setCurrentHighlightRangeOffset(((IDocumentKey) object).getOffset());
// We don't set the selected range because it will cause the
// manifest header and all its value to be selected
return;
}
// Handle manifest header values
// Determine the selection range
IDocumentRange range = findRange();
// Ensure there is a range
if (range == null) {
return;
}
// Get the model
IBaseModel model = getInputContext().getModel();
// Ensure we have an editing model
if ((model instanceof AbstractEditingModel) == false) {
return;
}
// If the range offset is undefined or the source viewer is dirty,
// forcibly adjust the offsets and try to find the range again
if ((range.getOffset() == -1) || isDirty()) {
try {
((AbstractEditingModel) model).adjustOffsets(((AbstractEditingModel) model).getDocument());
} catch (CoreException e) {
// Ignore
}
range = findRange();
}
// Set the highlight and selected range with whatever we found
setCurrentHighlightRangeOffset(range.getOffset());
setHighlightRange(range, true);
setSelectedRange(range, false);
}
@Override
protected void handleSelectionChangedSourcePage(SelectionChangedEvent event) {
super.handleSelectionChangedSourcePage(event);
ISelection selection = event.getSelection();
// Ensure we have a selection
if (selection.isEmpty() || ((selection instanceof ITextSelection) == false)) {
return;
}
// If the page has been edited, adjust the offsets; otherwise, our
// caculated ranges will be out of sync
IBaseModel model = getInputContext().getModel();
if (model instanceof AbstractEditingModel && isDirty()) {
try {
((AbstractEditingModel) model).adjustOffsets(((AbstractEditingModel) model).getDocument());
} catch (CoreException e) {
// Ignore
}
}
// Synchronize using the current cursor position in this page
synchronizeOutlinePage(((ITextSelection) selection).getOffset());
}
@Override
protected void synchronizeOutlinePage(int offset) {
// Prevent cyclical firing of events between source page and outline
// view
// If the previous offset is the same as the current offset, then
// the selection does not need to be updated in the outline view
int previous_offset = getCurrentHighlightRangeOffset();
if (previous_offset == offset) {
return;
}
// Find the range header (parent) or element (children) within range of
// the text selection offset
IDocumentRange rangeElement = getRangeElement(offset, true);
// Set the highlight range
updateHighlightRange(rangeElement);
// Set the outline view selection
updateOutlinePageSelection(getTargetOutlineSelection());
}
@Override
protected void editorContextMenuAboutToShow(IMenuManager menu) {
super.editorContextMenuAboutToShow(menu);
StyledText text = getViewer().getTextWidget();
Point p = text.getSelection();
IDocumentRange element = getRangeElement(p.x, false);
// only activate rename when user is highlighting Bundle-SymbolicName header
if (!(element instanceof BundleSymbolicNameHeader) || !(((BundleSymbolicNameHeader) element).getModel().isEditable()))
return;
if (fRenameAction == null) {
IBaseModel base = ((PDEFormEditor) getEditor()).getAggregateModel();
if (base instanceof IPluginModelBase) {
fRenameAction = RefactoringActionFactory.createRefactorPluginIdAction(NLS.bind(PDEUIMessages.BundleSourcePage_renameActionText, Constants.BUNDLE_SYMBOLICNAME));
fRenameAction.setSelection(base);
}
}
if (fRenameAction != null)
// add rename action after Outline. This is the same order as the hyperlink actions
menu.insertAfter(PDEActionConstants.COMMAND_ID_QUICK_OUTLINE, fRenameAction);
}
@Override
public void setActive(boolean active) {
super.setActive(active);
// Update the text selection if this page is being activated
if (active) {
updateTextSelection();
}
}
@Override
protected IFoldingStructureProvider getFoldingStructureProvider(IEditingModel model) {
return new BundleFoldingStructureProvider(this, model);
}
@Override
protected ChangeAwareSourceViewerConfiguration createSourceViewerConfiguration(IColorManager colorManager) {
return new ManifestConfiguration(colorManager, this);
}
}