/*******************************************************************************
* Copyright (c) 2009 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
* Zend Technologies
*******************************************************************************/
package org.eclipse.php.internal.ui.editor;
import java.util.ArrayList;
import java.util.EventObject;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.dltk.core.IModelElement;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.internal.ui.dialogs.OptionalMessageDialog;
import org.eclipse.emf.common.command.BasicCommandStack;
import org.eclipse.emf.common.command.CommandStack;
import org.eclipse.emf.common.command.CommandStackListener;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.internal.text.SelectionProcessor;
import org.eclipse.jface.text.*;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContentAssistant;
import org.eclipse.jface.text.contentassist.IContentAssistantExtension2;
import org.eclipse.jface.text.contentassist.IContentAssistantExtension4;
import org.eclipse.jface.text.formatter.FormattingContext;
import org.eclipse.jface.text.formatter.FormattingContextProperties;
import org.eclipse.jface.text.formatter.IContentFormatterExtension;
import org.eclipse.jface.text.formatter.IFormattingContext;
import org.eclipse.jface.text.information.IInformationPresenter;
import org.eclipse.jface.text.projection.ProjectionMapping;
import org.eclipse.jface.text.reconciler.DirtyRegion;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.source.*;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.php.core.ast.nodes.Program;
import org.eclipse.php.internal.core.documentModel.parser.PHPRegionContext;
import org.eclipse.php.internal.core.documentModel.parser.regions.IPHPScriptRegion;
import org.eclipse.php.internal.core.documentModel.partitioner.PHPPartitionTypes;
import org.eclipse.php.internal.ui.Logger;
import org.eclipse.php.internal.ui.PHPUIMessages;
import org.eclipse.php.internal.ui.PHPUiPlugin;
import org.eclipse.php.internal.ui.editor.configuration.PHPStructuredTextViewerConfiguration;
import org.eclipse.php.internal.ui.editor.contentassist.PHPCompletionProcessor;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.web.ui.SetupProjectsWizzard;
import org.eclipse.wst.sse.core.internal.parser.ForeignRegion;
import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.text.TextRegionListImpl;
import org.eclipse.wst.sse.core.internal.undo.IStructuredTextUndoManager;
import org.eclipse.wst.sse.ui.internal.SSEUIMessages;
import org.eclipse.wst.sse.ui.internal.StructuredDocumentToTextAdapter;
import org.eclipse.wst.sse.ui.internal.StructuredTextAnnotationHover;
import org.eclipse.wst.sse.ui.internal.StructuredTextViewer;
import org.eclipse.wst.sse.ui.internal.reconcile.StructuredRegionProcessor;
public class PHPStructuredTextViewer extends StructuredTextViewer {
/**
* Text operation code for requesting the outline for the current input.
*/
public static final int SHOW_OUTLINE = 51;
/**
* Text operation code for requesting the outline for the element at the
* current position.
*/
public static final int OPEN_STRUCTURE = 52;
/**
* Text operation code for requesting the hierarchy for the current input.
*/
public static final int SHOW_HIERARCHY = 53;
private static final String FORMAT_DOCUMENT_TEXT = SSEUIMessages.Format_Document_UI_;
private SourceViewerConfiguration fViewerConfiguration;
private ITextEditor fTextEditor;
private IInformationPresenter fOutlinePresenter;
private IInformationPresenter fHierarchyPresenter;
private IAnnotationHover fProjectionAnnotationHover;
private boolean fFireSelectionChanged = true;
private IDocumentAdapter documentAdapter;
private ContentAssistantFacade fContentAssistantFacade;
public PHPStructuredTextViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler,
boolean showAnnotationsOverview, int styles) {
super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles);
}
public PHPStructuredTextViewer(ITextEditor textEditor, Composite parent, IVerticalRuler verticalRuler,
IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles) {
super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles);
this.fTextEditor = textEditor;
if (fTextEditor instanceof PHPStructuredEditor) {
PHPStructuredEditor phpEditor = (PHPStructuredEditor) fTextEditor;
phpEditor.addReconcileListener(new IPHPScriptReconcilingListener() {
@Override
public void reconciled(Program program, boolean forced, IProgressMonitor progressMonitor) {
if (fPostSelectionLength != -1) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
synchronized (PHPStructuredTextViewer.this) {
if (fPostSelectionOffset >= 0 && fPostSelectionLength >= 0
&& getDocument() != null) {
firePostSelectionChanged(fPostSelectionOffset, fPostSelectionLength);
}
}
}
});
}
}
@Override
public void aboutToBeReconciled() {
}
});
}
}
public ITextEditor getTextEditor() {
return fTextEditor;
}
/**
* This method overrides WST since sometimes we get a subset of the document
* and NOT the whole document, although the case is FORMAT_DOCUMENT. In all
* other cases we call the parent method.
*/
@Override
public void doOperation(int operation) {
Point selection = getTextWidget().getSelection();
int cursorPosition = selection.x;
// save the last cursor position and the top visible line.
int selectionLength = selection.y - selection.x;
int topLine = getTextWidget().getTopIndex();
switch (operation) {
case FORMAT_DOCUMENT:
try {
setRedraw(false);
// begin recording
beginRecording(FORMAT_DOCUMENT_TEXT, FORMAT_DOCUMENT_TEXT, cursorPosition, selectionLength);
IRegion region;
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=486540
// format the whole document on save, not the active text
// selection
if (selectionLength != 0) {
region = new Region(cursorPosition, selectionLength);
} else {
region = new Region(0, getDocument().getLength());
}
if (fContentFormatter instanceof IContentFormatterExtension) {
IContentFormatterExtension extension = (IContentFormatterExtension) fContentFormatter;
IFormattingContext context = new FormattingContext();
context.setProperty(FormattingContextProperties.CONTEXT_DOCUMENT, Boolean.TRUE);
context.setProperty(FormattingContextProperties.CONTEXT_REGION, region);
extension.format(getDocument(), context);
} else {
fContentFormatter.format(getDocument(), region);
}
} finally {
// end recording
selection = getTextWidget().getSelection();
selectionLength = selection.y - selection.x;
endRecording(cursorPosition, selectionLength);
// return the cursor to its original position after the
// formatter change its position.
getTextWidget().setSelection(cursorPosition);
getTextWidget().setTopIndex(topLine);
setRedraw(true);
}
return;
case CONTENTASSIST_PROPOSALS:
// Handle javascript content assist when there is no support
// (instead of printing the stack trace)
if (fViewerConfiguration != null) {
IProject project = null;
boolean isJavaScriptRegion = false;
boolean hasJavaScriptNature = true;
try {
// Resolve the partition type
IStructuredDocument sDoc = (IStructuredDocument) getDocument();
// get the "real" offset - adjusted according to the
// projection
int selectionOffset = getSelectedRange().x;
IStructuredDocumentRegion sdRegion = sDoc.getRegionAtCharacterOffset(selectionOffset);
if (sdRegion == null) {
super.doOperation(operation);
return;
}
ITextRegion textRegion = sdRegion.getRegionAtCharacterOffset(selectionOffset);
if (textRegion instanceof ForeignRegion) {
ForeignRegion foreignRegion = (ForeignRegion) textRegion;
isJavaScriptRegion = "script" //$NON-NLS-1$
.equalsIgnoreCase(foreignRegion.getSurroundingTag());
}
// Check if the containing project has JS nature or not
if (fTextEditor instanceof PHPStructuredEditor) {
PHPStructuredEditor phpEditor = (PHPStructuredEditor) fTextEditor;
IModelElement modelElement = phpEditor.getModelElement();
if (modelElement != null) {
IScriptProject scriptProject = modelElement.getScriptProject();
project = scriptProject.getProject();
if (project != null && project.isAccessible()
&& project.getNature(JavaScriptCore.NATURE_ID) == null) {
hasJavaScriptNature = false;
}
}
}
// open dialog if required
if (isJavaScriptRegion && !hasJavaScriptNature) {
Shell activeWorkbenchShell = PHPUiPlugin.getActiveWorkbenchShell();
// Pop a question dialog - if the user selects 'Yes' JS
// Support is added, otherwise no change
int addJavaScriptSupport = OptionalMessageDialog.open("PROMPT_ADD_JAVASCRIPT_SUPPORT", //$NON-NLS-1$
activeWorkbenchShell, PHPUIMessages.PHPStructuredTextViewer_0, null,
PHPUIMessages.PHPStructuredTextViewer_1, OptionalMessageDialog.QUESTION,
new String[] { IDialogConstants.YES_LABEL, IDialogConstants.NO_LABEL }, 0); // $NON-NLS-1$
// run the JSDT action for adding the JS nature
if (addJavaScriptSupport == 0 && project != null) {
SetupProjectsWizzard wiz = new SetupProjectsWizzard();
wiz.setActivePart(null, this.getTextEditor());
wiz.selectionChanged(null, new StructuredSelection(project));
wiz.run(null);
}
}
} catch (CoreException e) {
Logger.logException(e);
}
}
// notifing the processors that the next request for completion is
// an explicit request
if (fViewerConfiguration != null) {
PHPStructuredTextViewerConfiguration structuredTextViewerConfiguration = (PHPStructuredTextViewerConfiguration) fViewerConfiguration;
IContentAssistProcessor[] all = structuredTextViewerConfiguration.getContentAssistProcessors(this,
PHPPartitionTypes.PHP_DEFAULT);
for (IContentAssistProcessor element : all) {
if (element instanceof PHPCompletionProcessor) {
((PHPCompletionProcessor) element).setExplicit(true);
}
}
}
super.doOperation(operation);
return;
case SHOW_OUTLINE:
if (fOutlinePresenter != null) {
fOutlinePresenter.showInformation();
}
return;
case SHOW_HIERARCHY:
if (fHierarchyPresenter != null) {
fHierarchyPresenter.showInformation();
}
return;
case DELETE:
StyledText textWidget = getTextWidget();
if (textWidget == null)
return;
ITextSelection textSelection = null;
if (redraws()) {
try {
textSelection = (ITextSelection) getSelection();
int length = textSelection.getLength();
if (!textWidget.getBlockSelection() && (length == 0 || length == textWidget.getSelectionRange().y))
getTextWidget().invokeAction(ST.DELETE_NEXT);
else
deleteSelection(textSelection, textWidget);
if (fFireSelectionChanged) {
Point range = textWidget.getSelectionRange();
fireSelectionChanged(range.x, range.y);
}
} catch (BadLocationException x) {
// ignore
}
}
return;
}
super.doOperation(operation);
}
public void setFireSelectionChanged(boolean fireSelectionChanged) {
this.fFireSelectionChanged = fireSelectionChanged;
}
/**
* Deletes the selection and sets the caret before the deleted range.
*
* @param selection
* the selection to delete
* @param textWidget
* the widget
* @throws BadLocationException
* on document access failure
* @since 3.5
*/
private void deleteSelection(ITextSelection selection, StyledText textWidget) throws BadLocationException {
new SelectionProcessor(this).doDelete(selection);
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.wst.sse.ui.internal.StructuredTextViewer#canDoOperation(int)
*/
@Override
public boolean canDoOperation(int operation) {
if (operation == SHOW_HIERARCHY) {
return fHierarchyPresenter != null;
}
if (operation == SHOW_OUTLINE) {
return fOutlinePresenter != null;
}
return super.canDoOperation(operation);
}
private void beginRecording(String label, String description, int cursorPosition, int selectionLength) {
IDocument doc = getDocument();
if (doc instanceof IStructuredDocument) {
IStructuredDocument structuredDocument = (IStructuredDocument) doc;
IStructuredTextUndoManager undoManager = structuredDocument.getUndoManager();
undoManager.beginRecording(this, label, description, cursorPosition, selectionLength);
} else {
// TODO: how to handle other document types?
}
}
private void endRecording(int cursorPosition, int selectionLength) {
IDocument doc = getDocument();
if (doc instanceof IStructuredDocument) {
IStructuredDocument structuredDocument = (IStructuredDocument) doc;
IStructuredTextUndoManager undoManager = structuredDocument.getUndoManager();
undoManager.endRecording(this, cursorPosition, selectionLength);
} else {
// TODO: how to handle other document types?
}
}
@Override
protected IDocumentAdapter createDocumentAdapter() {
documentAdapter = new StructuredDocumentToTextAdapterForPHP(getTextWidget());
return documentAdapter;
}
public IDocumentAdapter getDocumentAdapter() {
return documentAdapter;
}
/**
* (non-Javadoc)
*
* @see org.eclipse.jface.text.source.projection.ProjectionViewer#addVerticalRulerColumn(org.eclipse.jface.text.source.IVerticalRulerColumn)
*
* This method is only called to add Projection ruler column. It's
* actually a hack to override Projection presentation (information
* control) in order to enable syntax highlighting
*/
@Override
public void addVerticalRulerColumn(IVerticalRulerColumn column) {
// bug #210211 fix
if (fProjectionAnnotationHover == null) {
fProjectionAnnotationHover = new PHPStructuredTextProjectionAnnotationHover();
}
((AnnotationRulerColumn) column).setHover(fProjectionAnnotationHover);
super.addVerticalRulerColumn(column);
}
public class StructuredDocumentToTextAdapterForPHP extends StructuredDocumentToTextAdapter {
public StructuredDocumentToTextAdapterForPHP() {
super();
}
public StructuredDocumentToTextAdapterForPHP(StyledText styledTextWidget) {
super(styledTextWidget);
}
@Override
protected void redrawRegionChanged(RegionChangedEvent structuredDocumentEvent) {
if (structuredDocumentEvent != null && structuredDocumentEvent.getRegion() != null
&& structuredDocumentEvent.getRegion().getType() == PHPRegionContext.PHP_CONTENT) {
final IPHPScriptRegion region = (IPHPScriptRegion) structuredDocumentEvent.getRegion();
if (region.isFullReparsed()) {
final TextRegionListImpl newList = new TextRegionListImpl();
newList.add(region);
final IStructuredDocumentRegion structuredDocumentRegion = structuredDocumentEvent
.getStructuredDocumentRegion();
final IStructuredDocument structuredDocument = structuredDocumentEvent.getStructuredDocument();
final RegionsReplacedEvent regionsReplacedEvent = new RegionsReplacedEvent(structuredDocument,
structuredDocumentRegion, structuredDocumentRegion, null, newList, null, 0, 0);
redrawRegionsReplaced(regionsReplacedEvent);
}
}
super.redrawRegionChanged(structuredDocumentEvent);
}
}
/**
* We override this function in order to use content assist for php and not
* use the default one dictated by StructuredTextViewerConfiguration
*/
@Override
public void configure(SourceViewerConfiguration configuration) {
super.configure(configuration);
// release old annotation hover before setting new one
if (fAnnotationHover instanceof StructuredTextAnnotationHover) {
((StructuredTextAnnotationHover) fAnnotationHover).release();
}
// set PHP fAnnotationHover and initial the AnnotationHoverManager
setAnnotationHover(new PHPStructuredTextAnnotationHover());
ensureAnnotationHoverManagerInstalled();
if (!(configuration instanceof PHPStructuredTextViewerConfiguration)) {
return;
}
fViewerConfiguration = configuration;
PHPStructuredTextViewerConfiguration phpConfiguration = (PHPStructuredTextViewerConfiguration) configuration;
IContentAssistant newPHPAssistant = phpConfiguration.getPHPContentAssistant(this, true);
// Uninstall content assistant created in super:
if (fContentAssistant != null) {
fContentAssistant.uninstall();
}
// Assign, and configure our content assistant:
fContentAssistant = newPHPAssistant;
if (fContentAssistant != null) {
fContentAssistant.install(this);
if (fContentAssistant instanceof IContentAssistantExtension2
&& fContentAssistant instanceof IContentAssistantExtension4)
fContentAssistantFacade = new ContentAssistantFacade(fContentAssistant);
fContentAssistantInstalled = true;
} else {
// 248036 - disable the content assist operation if no content
// assistant
enableOperation(CONTENTASSIST_PROPOSALS, false);
}
fOutlinePresenter = phpConfiguration.getOutlinePresenter(this);
if (fOutlinePresenter != null) {
fOutlinePresenter.install(this);
}
fHierarchyPresenter = phpConfiguration.getHierarchyPresenter(this, true);
if (fHierarchyPresenter != null) {
fHierarchyPresenter.install(this);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.wst.sse.ui.internal.StructuredTextViewer#unconfigure()
*/
@Override
public void unconfigure() {
if (fHierarchyPresenter != null) {
fHierarchyPresenter.uninstall();
fHierarchyPresenter = null;
}
if (fOutlinePresenter != null) {
fOutlinePresenter.uninstall();
fOutlinePresenter = null;
}
super.unconfigure();
}
/**
* override the parent method to prevent initialization of wrong
* fAnnotationHover specific instance
*/
@Override
protected void ensureAnnotationHoverManagerInstalled() {
if (fAnnotationHover instanceof PHPStructuredTextAnnotationHover) {
super.ensureAnnotationHoverManagerInstalled();
}
}
/**
* (non-Javadoc)
*
* @see org.eclipse.wst.sse.ui.internal.StructuredTextViewer#modelLine2WidgetLine(int)
* Workaround for bug #195600 IllegalState is thrown by
* {@link ProjectionMapping#toImageLine(int)}
*/
@Override
public int modelLine2WidgetLine(int modelLine) {
try {
return super.modelLine2WidgetLine(modelLine);
} catch (IllegalStateException e) {
return -1;
}
}
/**
* (non-Javadoc)
*
* @see org.eclipse.jface.text.TextViewer#getClosestWidgetLineForModelLine(int)
* Workaround for bug #195600 IllegalState is thrown by
* {@link ProjectionMapping#toImageLine(int)}
*/
@Override
protected int getClosestWidgetLineForModelLine(int modelLine) {
try {
return super.getClosestWidgetLineForModelLine(modelLine);
} catch (IllegalStateException e) {
return -1;
}
}
/**
* Reconciles the whole document (to re-run PHPValidator)
*/
public void reconcile() {
((StructuredRegionProcessor) fReconciler).processDirtyRegion(
new DirtyRegion(0, getDocument().getLength(), DirtyRegion.INSERT, getDocument().get()));
}
/**
* Sets the given reconciler.
*
* @param reconciler
* the reconciler
*
*/
void setReconciler(IReconciler reconciler) {
fReconciler = reconciler;
}
/**
* Returns the reconciler.
*
* @return the reconciler or <code>null</code> if not set
*
*/
IReconciler getReconciler() {
return fReconciler;
}
/**
* Prepends the text presentation listener at the beginning of the viewer's
* list of text presentation listeners. If the listener is already
* registered with the viewer this call moves the listener to the beginning
* of the list.
*
* @param listener
* the text presentation listener
* @since 3.0
*/
@Override
public void prependTextPresentationListener(ITextPresentationListener listener) {
Assert.isNotNull(listener);
if (fTextPresentationListeners == null) {
fTextPresentationListeners = new ArrayList<>();
}
fTextPresentationListeners.remove(listener);
fTextPresentationListeners.add(0, listener);
}
/**
* Sends out a text selection changed event to all registered listeners and
* registers the selection changed event to be sent out to all post
* selection listeners.
*
* @param offset
* the offset of the newly selected range in the visible document
* @param length
* the length of the newly selected range in the visible document
*/
@Override
protected void selectionChanged(int offset, int length) {
if (fFireSelectionChanged) {
super.selectionChanged(offset, length);
}
}
public SourceViewerConfiguration getViewerConfiguration() {
return fViewerConfiguration;
}
@Override
public void setDocument(IDocument document, IAnnotationModel annotationModel, int modelRangeOffset,
int modelRangeLength) {
if (getDocument() instanceof IStructuredDocument) {
CommandStack commandStack = ((IStructuredDocument) getDocument()).getUndoManager().getCommandStack();
if (commandStack instanceof BasicCommandStack) {
commandStack.addCommandStackListener(getInternalCommandStackListener());
}
}
super.setDocument(document, annotationModel, modelRangeOffset, modelRangeLength);
if (getDocument() instanceof IStructuredDocument) {
CommandStack commandStack = ((IStructuredDocument) getDocument()).getUndoManager().getCommandStack();
if (commandStack instanceof BasicCommandStack) {
commandStack.addCommandStackListener(getInternalCommandStackListener());
}
}
}
private void fireDirty() {
if (fTextEditor instanceof PHPStructuredEditor) {
PHPStructuredEditor phpEditor = (PHPStructuredEditor) fTextEditor;
phpEditor.firePropertyChange(ITextEditor.PROP_DIRTY);
}
}
private InternalCommandStackListener fInternalCommandStackListener;
private int fPostSelectionLength;
private int fPostSelectionOffset;
/**
* @return
*/
private CommandStackListener getInternalCommandStackListener() {
if (fInternalCommandStackListener == null) {
fInternalCommandStackListener = new InternalCommandStackListener();
}
return fInternalCommandStackListener;
}
class InternalCommandStackListener implements CommandStackListener {
@Override
public void commandStackChanged(EventObject event) {
fireDirty();
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.wst.sse.ui.internal.StructuredTextViewer#
* getContentAssistFacade ()
*/
@Override
public ContentAssistantFacade getContentAssistFacade() {
return fContentAssistantFacade;
}
@Override
protected void firePostSelectionChanged(int offset, int length) {
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=500993
// This method must be synchronized to avoid using negative
// fPostSelectionOffset values (see concurrent access with the
// IPhpScriptReconcilingListener attached to PHPStructuredEditor and
// defined in the PHPStructuredTextViewer constructor).
synchronized (this) {
if (fTextEditor instanceof PHPStructuredEditor
&& !((PHPStructuredEditor) fTextEditor).fReconcileSelection) {
super.firePostSelectionChanged(offset, length);
fPostSelectionOffset = -1;
fPostSelectionLength = -1;
} else {
fPostSelectionOffset = offset;
fPostSelectionLength = length;
}
}
}
}