/*******************************************************************************
* Copyright (c) 2014 Salesforce.com, inc..
* 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:
* Salesforce.com, inc. - initial API and implementation
******************************************************************************/
package com.salesforce.ide.ui.editors.apex;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import org.apache.log4j.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.internal.ui.text.JavaPairMatcher;
import org.eclipse.jdt.internal.ui.text.PreferencesAdapter;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISharedTextColors;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.text.source.projection.ProjectionSupport;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPartService;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.editors.text.ITextEditorHelpContextIds;
import org.eclipse.ui.editors.text.TextEditor;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.texteditor.ChainedPreferenceStore;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.eclipse.ui.texteditor.TextEditorAction;
import org.eclipse.ui.texteditor.TextOperationAction;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import com.salesforce.ide.core.internal.context.ContainerDelegate;
import com.salesforce.ide.core.internal.utils.Constants;
import com.salesforce.ide.core.internal.utils.Utils;
import com.salesforce.ide.core.project.ForceProjectException;
import com.salesforce.ide.ui.ForceIdeUIPlugin;
import com.salesforce.ide.ui.editors.ForceIdeEditorsPlugin;
import com.salesforce.ide.ui.editors.apex.outline.ApexContentOutlinePage;
import com.salesforce.ide.ui.editors.apex.outline.OutlineViewDispatcher;
import com.salesforce.ide.ui.editors.apex.preferences.PreferenceConstants;
import com.salesforce.ide.ui.editors.apex.util.ParserLocationTranslator.HighlightRange;
import com.salesforce.ide.ui.editors.internal.utils.EditorMessages;
/**
* Apex specific code editor
*
* @author cwall (Modified by nchen)
*/
@SuppressWarnings("restriction")
public class ApexCodeEditor extends TextEditor implements IShowInSource {
private static final Logger logger = Logger.getLogger(ApexCodeEditor.class);
public static final String EDITOR_NAME = "Apex Code Editor";
private static final String ANNOTATION_TYPE_APEX_WARNING = "org.eclipse.ui.workbench.texteditor.warning";
private static final String ANNOTATION_TYPE_APEX_ERROR = "org.eclipse.ui.workbench.texteditor.error";
private static final String ACTION_DEFINE_FOLDING_REGION = "DefineFoldingRegion";
private static final String ACTION_CONTENT_ASSIST_TIP = "ContentAssistTip";
private static final String ACTION_CONTENT_ASSIST_PROPOSAL = "ContentAssistProposal";
protected final static char[] BRACKETS = { '{', '}', '(', ')', '[', ']', '<', '>' };
/** The editor's bracket matcher */
protected JavaPairMatcher fBracketMatcher = new JavaPairMatcher(BRACKETS);
/** The bracket inserter. */
private BracketInserter fBracketInserter;
protected ApexContentOutlinePage fOutlinePage; // The outline page
private ProjectionSupport fProjectionSupport; // The projection support
private IProject project = null;
private final OutlineUpdateResourceListener outlineUpdateResourceListener = new OutlineUpdateResourceListener();
protected AbstractSelectionChangedListener outlineSelectionChangedListener = new OutlineSelectionChangedListener();
private EditorSelectionChangedListener editorSelectionChangedListener;
private ApexSourceViewerConfiguration apexSourceViewerConfiguration = null;
private final Object fReconcilerLock = new Object();
public Object getReconcilerLock() {
return fReconcilerLock;
}
public ApexCodeEditor(IProject project) {
super();
this.project = project;
initializeEditor();
IWorkspace workspace = ResourcesPlugin.getWorkspace();
workspace.addResourceChangeListener(outlineUpdateResourceListener);
}
// M E T H O D S
@Override
public ShowInContext getShowInContext() {
ShowInContext context = new ShowInContext(getEditorInput(), getSelectionProvider().getSelection());
return context;
}
public IProject getProject() {
return project;
}
public void setProject(IProject project) {
this.project = project;
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method extend the actions to
* add those specific to the receiver
*/
@Override
protected void createActions() {
super.createActions();
IAction action =
new TextOperationAction(EditorMessages.getResourceBundle(),
"ApexEditor.ContentAssistProposal.", this, ISourceViewer.CONTENTASSIST_PROPOSALS); //$NON-NLS-1$
action.setActionDefinitionId(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
setAction(ACTION_CONTENT_ASSIST_PROPOSAL, action);
action =
new TextOperationAction(EditorMessages.getResourceBundle(),
"ApexEditor.ContentAssistTip.", this, ISourceViewer.CONTENTASSIST_CONTEXT_INFORMATION); //$NON-NLS-1$
action.setActionDefinitionId(ITextEditorActionDefinitionIds.CONTENT_ASSIST_CONTEXT_INFORMATION);
setAction(ACTION_CONTENT_ASSIST_TIP, action);
action =
new DefineFoldingRegionAction(EditorMessages.getResourceBundle(),
"ApexEditor.DefineFoldingRegion.", this); //$NON-NLS-1$
setAction(ACTION_DEFINE_FOLDING_REGION, action);
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method performs any extra
* disposal actions required by the java editor.
*/
@Override
public void dispose() {
if (fOutlinePage != null) {
fOutlinePage.dispose();
}
if (outlineUpdateResourceListener != null) {
outlineUpdateResourceListener.clear();
removeListenerObject(outlineUpdateResourceListener);
}
if (outlineSelectionChangedListener != null) {
outlineSelectionChangedListener.uninstall(getSelectionProvider());
outlineSelectionChangedListener = null;
}
if (editorSelectionChangedListener != null) {
editorSelectionChangedListener.uninstall(getSelectionProvider());
editorSelectionChangedListener = null;
}
if (fBracketMatcher != null) {
fBracketMatcher.dispose();
fBracketMatcher = null;
}
if (getSourceViewer() instanceof ITextViewerExtension)
((ITextViewerExtension) getSourceViewer()).removeVerifyKeyListener(fBracketInserter);
super.dispose();
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method performs any extra
* revert behavior required by the java editor.
*/
@Override
public void doRevertToSaved() {
outlineUpdateResourceListener.setFilePath(getFile().getFullPath());
super.doRevertToSaved();
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method performs any extra save
* behavior required by the java editor.
*
* @param monitor
* the progress monitor
*/
@Override
public void doSave(IProgressMonitor monitor) {
outlineUpdateResourceListener.setFilePath(getFile().getFullPath());
super.doSave(monitor);
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method performs any extra save
* as behavior required by the java editor.
*/
@Override
public void doSaveAs() {
outlineUpdateResourceListener.setFilePath(getFile().getFullPath());
super.doSaveAs();
}
@Override
public void close(boolean save) {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
workspace.removeResourceChangeListener(outlineUpdateResourceListener);
super.close(save);
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method performs sets the input
* of the outline page after AbstractTextEditor has set input.
*
* @param input
* the editor input
* @throws CoreException
* in case the input can not be set
*/
@Override
public void doSetInput(IEditorInput input) throws CoreException {
if (!input.exists()) {
logger.warn("Input does not exist for Apex Code Editor");
return;
}
super.doSetInput(input);
IFile file = getFile();
if (file == null) {
logger.warn("File for editor input is null");
return;
}
outlineUpdateResourceListener.setFilePath(file.getFullPath());
ContainerDelegate.getInstance().getServiceLocator().getProjectService().setResourceAttributes(file);
setProject(file.getProject());
if (logger.isInfoEnabled()) {
logger.info("Set resource attributes on '" + file.getProjectRelativePath().toPortableString() + "'");
}
}
private IFile getFile() {
return (getEditorInput() instanceof IFileEditorInput) ? ((IFileEditorInput) getEditorInput()).getFile() : null;
}
/*
* @see org.eclipse.ui.texteditor.ExtendedTextEditor#editorContextMenuAboutToShow(org.eclipse.jface.action.IMenuManager)
*/
@Override
public void editorContextMenuAboutToShow(IMenuManager menu) {
super.editorContextMenuAboutToShow(menu);
addAction(menu, ACTION_CONTENT_ASSIST_PROPOSAL);
addAction(menu, ACTION_CONTENT_ASSIST_TIP);
addAction(menu, ACTION_DEFINE_FOLDING_REGION);
}
/**
* The <code>JavaEditor</code> implementation of this <code>AbstractTextEditor</code> method performs gets the java
* content outline page if request is for a an outline page.
*
* @param required
* the required type
* @return an adapter for the required type or <code>null</code>
*/
@Override
@SuppressWarnings("unchecked")
public Object getAdapter(Class required) {
if (IContentOutlinePage.class.equals(required)) {
if (fOutlinePage == null) {
fOutlinePage = new ApexContentOutlinePage();
if (getEditorInput() != null) {
outlineSelectionChangedListener.install(fOutlinePage);
}
}
return fOutlinePage;
}
if (fProjectionSupport != null) {
Object adapter = fProjectionSupport.getAdapter(getSourceViewer(), required);
if (adapter != null) {
return adapter;
}
}
return super.getAdapter(required);
}
/*
* (non-Javadoc) Method declared on AbstractTextEditor
*/
@Override
protected void initializeEditor() {
setEditorContextMenuId("#ApexDebuggerEditorContext"); //$NON-NLS-1$
setRulerContextMenuId("#ApexDebuggerRulerContext"); //$NON-NLS-1$
setHelpContextId(ITextEditorHelpContextIds.TEXT_EDITOR);
IPreferenceStore store = createCombinedPreferenceStore(null);
setPreferenceStore(store);
setHelpContextId(Constants.DOCUMENTATION_PLUGIN_PREFIX + "." + this.getClass().getSimpleName());
try {
apexSourceViewerConfiguration = new ApexSourceViewerConfiguration(getPreferenceStore(), this);
apexSourceViewerConfiguration.init(project);
setSourceViewerConfiguration(apexSourceViewerConfiguration);
} catch (ForceProjectException e) {
logger.error("Unable to initialize source viewer configuration", e);
Utils.openError("Initialization Error",
"Unable to initialize source viewer configuration: " + e.getMessage());
}
}
/**
* Creates and returns the preference store for this Java editor with the given input.
*
* @param input
* The editor input for which to create the preference store
* @return the preference store for this editor
*
*/
private IPreferenceStore createCombinedPreferenceStore(IEditorInput input) {
List<IPreferenceStore> stores = new ArrayList<IPreferenceStore>(3);
stores.add(ForceIdeEditorsPlugin.getDefault().getPreferenceStore());
stores.add(new PreferencesAdapter(ForceIdeEditorsPlugin.getDefault().getPluginPreferences()));
stores.add(EditorsUI.getPreferenceStore());
return new ChainedPreferenceStore(stores.toArray(new IPreferenceStore[stores.size()]));
}
/*
* @see org.eclipse.ui.texteditor.ExtendedTextEditor#createPartControl(org.eclipse.swt.widgets.Composite)
*/
@Override
public void createPartControl(Composite parent) {
super.createPartControl(parent);
((ProjectionViewer) getSourceViewer()).enableProjection();
editorSelectionChangedListener = new EditorSelectionChangedListener();
editorSelectionChangedListener.install(getSelectionProvider());
IPreferenceStore preferenceStore = getPreferenceStore();
boolean closeBrackets = preferenceStore.getBoolean(PreferenceConstants.EDITOR_CLOSE_BRACKETS);
boolean closeStrings = preferenceStore.getBoolean(PreferenceConstants.EDITOR_CLOSE_STRINGS);
boolean closeAngularBrackets =
"1.5".compareTo(preferenceStore.getString(PreferenceConstants.COMPILER_SOURCE)) <= 0; // $NON-NLS-1$
fBracketInserter.setCloseBracketsEnabled(closeBrackets);
fBracketInserter.setCloseStringsEnabled(closeStrings);
fBracketInserter.setCloseAngularBracketsEnabled(closeAngularBrackets);
ISourceViewer sourceViewer = getSourceViewer();
if (sourceViewer instanceof ITextViewerExtension)
((ITextViewerExtension) sourceViewer).prependVerifyKeyListener(fBracketInserter);
}
/*
* @see org.eclipse.ui.texteditor.ExtendedTextEditor#createSourceViewer(org.eclipse.swt.widgets.Composite,
* org.eclipse.jface.text.source.IVerticalRuler, int)
*/
@Override
protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler ruler, int styles) {
fAnnotationAccess = createAnnotationAccess();
fOverviewRuler = createOverviewRuler(getSharedColors());
ProjectionViewer viewer =
new ProjectionViewer(parent, ruler, getOverviewRuler(), isOverviewRulerVisible(), styles);
fProjectionSupport = new ProjectionSupport(viewer, getAnnotationAccess(), getSharedColors());
fProjectionSupport.addSummarizableAnnotationType(ANNOTATION_TYPE_APEX_ERROR); //$NON-NLS-1$
fProjectionSupport.addSummarizableAnnotationType(ANNOTATION_TYPE_APEX_WARNING); //$NON-NLS-1$
fProjectionSupport.install();
// viewer.doOperation(ProjectionViewer.TOGGLE);
setTitleToolTip(EDITOR_NAME);
// ensure source viewer decoration support has been created and configured
getSourceViewerDecorationSupport(viewer);
fBracketInserter = new BracketInserter(this, viewer);
return viewer;
}
@Override
protected ISharedTextColors getSharedColors() {
ISharedTextColors sharedColors = ForceIdeUIPlugin.getSharedTextColors();
return sharedColors;
}
/*
* @see org.eclipse.ui.texteditor.AbstractTextEditor#adjustHighlightRange(int,
* int)
*/
@Override
protected void adjustHighlightRange(int offset, int length) {
ISourceViewer viewer = getSourceViewer();
if (viewer instanceof ITextViewerExtension5) {
ITextViewerExtension5 extension = (ITextViewerExtension5) viewer;
extension.exposeModelRange(new Region(offset, length));
}
}
protected static int selectWord(ITextViewer fText, final int caretPos) {
IDocument doc = fText.getDocument();
int startPos, endPos;
try {
int pos = caretPos;
char c;
while (pos >= 0) {
c = doc.getChar(pos);
if (!Character.isJavaIdentifierPart(c)) {
break;
}
--pos;
}
startPos = pos;
pos = caretPos;
int length = doc.getLength();
while (pos < length) {
c = doc.getChar(pos);
if (!Character.isJavaIdentifierPart(c)) {
break;
}
++pos;
}
endPos = pos;
selectRange(startPos, endPos, fText);
return endPos;
} catch (BadLocationException x) {
// do nothing return a -1
}
return -1;
}
private static void selectRange(int startPos, int stopPos, ITextViewer fText) {
int offset = startPos + 1;
int length = stopPos - offset;
fText.setSelectedRange(offset, length);
}
private class DefineFoldingRegionAction extends TextEditorAction {
public DefineFoldingRegionAction(ResourceBundle bundle, String prefix, ITextEditor editor) {
super(bundle, prefix, editor);
}
private IAnnotationModel getAnnotationModel(ITextEditor editor) {
return (IAnnotationModel) editor.getAdapter(ProjectionAnnotationModel.class);
}
/*
* @see org.eclipse.jface.action.Action#run()
*/
@Override
public void run() {
ITextEditor editor = getTextEditor();
ISelection selection = editor.getSelectionProvider().getSelection();
if (selection instanceof ITextSelection) {
ITextSelection textSelection = (ITextSelection) selection;
if (!textSelection.isEmpty()) {
IAnnotationModel model = getAnnotationModel(editor);
if (model != null) {
int start = textSelection.getStartLine();
int end = textSelection.getEndLine();
try {
IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput());
int offset = document.getLineOffset(start);
int endOffset = document.getLineOffset(end + 1);
Position position = new Position(offset, endOffset - offset);
model.addAnnotation(new ProjectionAnnotation(), position);
} catch (BadLocationException x) {
// ignore
}
}
}
}
}
}
class OutlineUpdateResourceListener implements IResourceChangeListener {
protected IPath filePath = null;
private int incrementer = 1;
public OutlineUpdateResourceListener() {
}
public IPath getFilePath() {
return filePath;
}
public void setFilePath(IPath filePath) {
resetIncrementer();
this.filePath = filePath;
}
public void clear() {
resetIncrementer();
this.filePath = null;
}
public void resetIncrementer() {
incrementer = 1;
}
@Override
public void resourceChanged(IResourceChangeEvent event) {
if (filePath == null) {
return;
}
// we are only interested in POST_CHANGE events and post-build
// changes since we're dependent upon
// the server updates - changes to identifier tables
if (event.getType() != IResourceChangeEvent.POST_CHANGE
|| (event.getType() == IResourceChangeEvent.POST_CHANGE && incrementer < 2)) {
incrementer++;
return;
}
resetIncrementer();
IResourceDelta rootDelta = event.getDelta();
// get the delta, if any, for the documentation directory
IResourceDelta docDelta = rootDelta.findMember(filePath);
if (docDelta == null) {
return;
}
IResourceDeltaVisitor visitor = new IResourceDeltaVisitor() {
@Override
public boolean visit(IResourceDelta delta) {
// only interested in changed resources (not added or
// removed)
if (delta.getKind() != IResourceDelta.CHANGED) {
return true;
}
// only interested in content changes
if ((delta.getFlags() & IResourceDelta.CONTENT) == 0) {
return true;
}
IResource resource = delta.getResource();
// only interested in files with the "txt" extension
if (resource.getType() == IResource.FILE && filePath.equals(resource.getFullPath())) {
if (logger.isInfoEnabled()) {
logger.info("Updating outline view for '" + filePath.toPortableString() + "'");
}
}
return true;
}
};
try {
docDelta.accept(visitor);
} catch (CoreException e) {
String logMessage = Utils.generateCoreExceptionLog(e);
logger.warn("Unable to update Apex Code outline view: " + logMessage);
}
}
}
/**
* Updates the selection in the editor's widget with the selection of the outline page.
*/
class OutlineSelectionChangedListener extends AbstractSelectionChangedListener {
OutlineViewDispatcher<HighlightRange> dispatcher = new OutlineViewDispatcher<HighlightRange>(new OutlineViewSelectionHandler(
ApexCodeEditor.this));
@Override
public void selectionChanged(SelectionChangedEvent event) {
ISelection selection = event.getSelection();
if (selection instanceof ITreeSelection) {
Object selectedObject = ((ITreeSelection) selection).getFirstElement();
dispatcher.dispatch(selectedObject);
}
}
}
protected void setSelection(HighlightRange range, boolean moveCursor) {
ISelection selection = getSelectionProvider().getSelection();
if (selection instanceof ITextSelection) {
ITextSelection textSelection = (ITextSelection) selection;
if (moveCursor && (textSelection.getOffset() != 0 || textSelection.getLength() != 0))
markInNavigationHistory();
}
StyledText textWidget = null;
ISourceViewer sourceViewer = getSourceViewer();
if (sourceViewer != null)
textWidget = sourceViewer.getTextWidget();
if (textWidget == null)
return;
try {
int offset = range.startOffset;
int length = range.length;
if (offset < 0 || length < 0)
return;
setHighlightRange(offset, length, moveCursor);
if (!moveCursor)
return;
if (offset > -1 && length > 0) {
try {
textWidget.setRedraw(false);
sourceViewer.revealRange(offset, length);
sourceViewer.setSelectedRange(offset, length);
} finally {
textWidget.setRedraw(true);
}
markInNavigationHistory();
}
} catch (IllegalArgumentException x) {}
}
private class EditorSelectionChangedListener extends AbstractSelectionChangedListener {
@Override
public void selectionChanged(SelectionChangedEvent event) {}
}
protected boolean isActivePart() {
IWorkbenchPart part = getActivePart();
return part != null && part.equals(this);
}
private IWorkbenchPart getActivePart() {
IWorkbenchWindow window = getSite().getWorkbenchWindow();
IPartService service = window.getPartService();
IWorkbenchPart part = service.getActivePart();
return part;
}
public String getText() {
return getDocument().get();
}
public IDocument getDocument() {
return getDocumentProvider().getDocument(getEditorInput());
}
public int getTabWidth() {
final int[] tabs = new int[1];
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
tabs[0] = getSourceViewer().getTextWidget().getTabs();
}
});
return tabs[0];
}
/* (non-Javadoc)
* adds matcher for chars in BRACKETS member variable
*/
@Override
protected void configureSourceViewerDecorationSupport(SourceViewerDecorationSupport support) {
// JavaPairMatcher only matches "<>" for certain compiler versions
fBracketMatcher.setSourceVersion(getPreferenceStore().getString(PreferenceConstants.COMPILER_SOURCE));
support.setCharacterPairMatcher(fBracketMatcher);
support.setMatchingCharacterPainterPreferenceKeys(PreferenceConstants.EDITOR_MATCHING_BRACKETS,
PreferenceConstants.EDITOR_MATCHING_BRACKETS_COLOR);
super.configureSourceViewerDecorationSupport(support);
}
}