/*******************************************************************************
* Copyright (c) 2000, 2008 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
*******************************************************************************/
package org.eclipse.wst.jsdt.internal.ui.text.correction;
import java.util.Iterator;
import org.eclipse.compare.rangedifferencer.IRangeComparator;
import org.eclipse.compare.rangedifferencer.RangeDifference;
import org.eclipse.compare.rangedifferencer.RangeDifferencer;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.ProposalPosition;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.DocumentChange;
import org.eclipse.ltk.core.refactoring.TextChange;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
import org.eclipse.wst.jsdt.core.JavaScriptModelException;
import org.eclipse.wst.jsdt.internal.corext.codemanipulation.StubUtility;
import org.eclipse.wst.jsdt.internal.corext.fix.LinkedProposalModel;
import org.eclipse.wst.jsdt.internal.corext.fix.LinkedProposalPositionGroup;
import org.eclipse.wst.jsdt.internal.corext.refactoring.changes.CompilationUnitChange;
import org.eclipse.wst.jsdt.internal.corext.util.Resources;
import org.eclipse.wst.jsdt.internal.corext.util.Strings;
import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
import org.eclipse.wst.jsdt.internal.ui.JavaUIStatus;
import org.eclipse.wst.jsdt.internal.ui.compare.JavaTokenComparator;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorHighlightingSynchronizer;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.JavaEditor;
import org.eclipse.wst.jsdt.internal.ui.util.ExceptionHandler;
import org.eclipse.wst.jsdt.ui.JavaScriptUI;
import org.eclipse.wst.jsdt.ui.text.java.IJavaCompletionProposal;
/**
* A proposal for quick fixes and quick assist that work on a single compilation unit.
* Either a {@link TextChange text change} is directly passed in the constructor or method
* {@link #addEdits(IDocument, TextEdit)} is overridden to provide the text edits that are
* applied to the document when the proposal is evaluated.
* <p>
* The proposal takes care of the preview of the changes as proposal information.
* </p>
*
*/
public class CUCorrectionProposal extends ChangeCorrectionProposal {
private IJavaScriptUnit fCompilationUnit;
private LinkedProposalModel fLinkedProposalModel;
/**
* Constructs a correction proposal working on a compilation unit with a given text change
*
* @param name the name that is displayed in the proposal selection dialog.
* @param cu the compilation unit on that the change works.
* @param change the change that is executed when the proposal is applied or <code>null</code>
* if implementors override {@link #addEdits(IDocument, TextEdit)} to provide
* the text edits or {@link #createTextChange()} to provide a text change.
* @param relevance the relevance of this proposal.
* @param image the image that is displayed for this proposal or <code>null</code> if no
* image is desired.
*/
public CUCorrectionProposal(String name, IJavaScriptUnit cu, TextChange change, int relevance, Image image) {
super(name, change, relevance, image);
if (cu == null) {
throw new IllegalArgumentException("Compilation unit must not be null"); //$NON-NLS-1$
}
fCompilationUnit= cu;
fLinkedProposalModel= null;
}
/**
* Constructs a correction proposal working on a compilation unit.
* <p>Users have to override {@link #addEdits(IDocument, TextEdit)} to provide
* the text edits or {@link #createTextChange()} to provide a text change.
* </p>
*
* @param name The name that is displayed in the proposal selection dialog.
* @param cu The compilation unit on that the change works.
* @param relevance The relevance of this proposal.
* @param image The image that is displayed for this proposal or <code>null</code> if no
* image is desired.
*/
protected CUCorrectionProposal(String name, IJavaScriptUnit cu, int relevance, Image image) {
this(name, cu, null, relevance, image);
}
/**
* Called when the {@link CompilationUnitChange} is initialized. Subclasses can override to
* add text edits to the root edit of the change. Implementors must not access the proposal,
* e.g getting the change.
* <p>The default implementation does not add any edits</p>
*
* @param document content of the underlying compilation unit. To be accessed read only.
* @param editRoot The root edit to add all edits to
* @throws CoreException can be thrown if adding the edits is failing.
*/
protected void addEdits(IDocument document, TextEdit editRoot) throws CoreException {
if (false) {
throw new CoreException(JavaUIStatus.createError(IStatus.ERROR, "Implementors can throw an exception", null)); //$NON-NLS-1$
}
}
protected LinkedProposalModel getLinkedProposalModel() {
if (fLinkedProposalModel == null) {
fLinkedProposalModel= new LinkedProposalModel();
}
return fLinkedProposalModel;
}
protected void setLinkedProposalModel(LinkedProposalModel model) {
fLinkedProposalModel= model;
}
/*
* @see ICompletionProposal#getAdditionalProposalInfo()
*/
public String getAdditionalProposalInfo() {
StringBuffer buf= new StringBuffer();
try {
TextChange change= getTextChange();
IDocument previewContent= change.getPreviewDocument(new NullProgressMonitor());
String currentConentString= change.getCurrentContent(new NullProgressMonitor());
/*
* Do not change the type of those local variables. We use Object
* here in order to prevent loading of the Compare plug-in at load
* time of this class.
*/
Object leftSide= new JavaTokenComparator(previewContent.get());
Object rightSide= new JavaTokenComparator(currentConentString);
RangeDifference[] differences= RangeDifferencer.findRanges((IRangeComparator)leftSide, (IRangeComparator)rightSide);
for (int i= 0; i < differences.length; i++) {
RangeDifference curr= differences[i];
int start= ((JavaTokenComparator)leftSide).getTokenStart(curr.leftStart());
int end= ((JavaTokenComparator)leftSide).getTokenStart(curr.leftEnd());
if (curr.kind() == RangeDifference.CHANGE && curr.leftLength() > 0) {
buf.append("<b>"); //$NON-NLS-1$
appendContent(previewContent, start, end, buf, false);
buf.append("</b>"); //$NON-NLS-1$
} else if (curr.kind() == RangeDifference.NOCHANGE) {
appendContent(previewContent, start, end, buf, true);
}
}
} catch (CoreException e) {
JavaScriptPlugin.log(e);
} catch (BadLocationException e) {
JavaScriptPlugin.log(e);
}
return buf.toString();
}
private final int surroundLines= 1;
private void appendContent(IDocument text, int startOffset, int endOffset, StringBuffer buf, boolean surroundLinesOnly) throws BadLocationException {
int startLine= text.getLineOfOffset(startOffset);
int endLine= text.getLineOfOffset(endOffset);
boolean dotsAdded= false;
if (surroundLinesOnly && startOffset == 0) { // no surround lines for the top no-change range
startLine= Math.max(endLine - surroundLines, 0);
buf.append("...<br>"); //$NON-NLS-1$
dotsAdded= true;
}
for (int i= startLine; i <= endLine; i++) {
if (surroundLinesOnly) {
if ((i - startLine > surroundLines) && (endLine - i > surroundLines)) {
if (!dotsAdded) {
buf.append("...<br>"); //$NON-NLS-1$
dotsAdded= true;
} else if (endOffset == text.getLength()) {
return; // no surround lines for the bottom no-change range
}
continue;
}
}
IRegion lineInfo= text.getLineInformation(i);
int start= lineInfo.getOffset();
int end= start + lineInfo.getLength();
int from= Math.max(start, startOffset);
int to= Math.min(end, endOffset);
String content= text.get(from, to - from);
if (surroundLinesOnly && (from == start) && Strings.containsOnlyWhitespaces(content)) {
continue; // ignore empty lines except when range started in the middle of a line
}
for (int k= 0; k < content.length(); k++) {
char ch= content.charAt(k);
if (ch == '<') {
buf.append("<"); //$NON-NLS-1$
} else if (ch == '>') {
buf.append(">"); //$NON-NLS-1$
} else {
buf.append(ch);
}
}
if (to == end && to != endOffset) { // new line when at the end of the line, and not end of range
buf.append("<br>"); //$NON-NLS-1$
}
}
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.contentassist.ICompletionProposal#apply(org.eclipse.jface.text.IDocument)
*/
public void apply(IDocument document) {
try {
IJavaScriptUnit unit= getCompilationUnit();
IEditorPart part= null;
if (unit.getResource().exists()) {
boolean canEdit= performValidateEdit(unit);
if (!canEdit) {
return;
}
part= EditorUtility.isOpenInEditor(unit);
if (part == null) {
part= JavaScriptUI.openInEditor(unit);
if (part != null) {
document= JavaScriptUI.getDocumentProvider().getDocument(part.getEditorInput());
}
}
IWorkbenchPage page= JavaScriptPlugin.getActivePage();
if (page != null && part != null) {
page.bringToTop(part);
}
if (part != null) {
part.setFocus();
}
}
performChange(part, document);
} catch (CoreException e) {
ExceptionHandler.handle(e, CorrectionMessages.CUCorrectionProposal_error_title, CorrectionMessages.CUCorrectionProposal_error_message);
}
}
private boolean performValidateEdit(IJavaScriptUnit unit) {
IStatus status= Resources.makeCommittable(unit.getResource(), JavaScriptPlugin.getActiveWorkbenchShell());
if (!status.isOK()) {
String label= CorrectionMessages.CUCorrectionProposal_error_title;
String message= CorrectionMessages.CUCorrectionProposal_error_message;
ErrorDialog.openError(JavaScriptPlugin.getActiveWorkbenchShell(), label, message, status);
return false;
}
return true;
}
/* (non-Javadoc)
* @see org.eclipse.wst.jsdt.internal.ui.text.correction.ChangeCorrectionProposal#performChange(org.eclipse.jface.text.IDocument, org.eclipse.ui.IEditorPart)
*/
protected void performChange(IEditorPart part, IDocument document) throws CoreException {
try {
super.performChange(part, document);
if (part == null) {
return;
}
if (fLinkedProposalModel != null) {
if (fLinkedProposalModel.hasLinkedPositions() && part instanceof JavaEditor) {
// enter linked mode
ITextViewer viewer= ((JavaEditor) part).getViewer();
enterLinkedMode(viewer, part);
} else if (part instanceof ITextEditor) {
LinkedProposalPositionGroup.PositionInformation endPosition= fLinkedProposalModel.getEndPosition();
if (endPosition != null) {
// select a result
int pos= endPosition.getOffset() + endPosition.getLength();
((ITextEditor) part).selectAndReveal(pos, 0);
}
}
}
} catch (BadLocationException e) {
throw new CoreException(JavaUIStatus.createError(IStatus.ERROR, e));
}
}
private void enterLinkedMode(ITextViewer viewer, IEditorPart editor) throws BadLocationException {
IDocument document= viewer.getDocument();
LinkedModeModel model= new LinkedModeModel();
boolean added= false;
Iterator iterator= fLinkedProposalModel.getPositionGroupIterator();
while (iterator.hasNext()) {
LinkedProposalPositionGroup curr= (LinkedProposalPositionGroup) iterator.next();
LinkedPositionGroup group= new LinkedPositionGroup();
LinkedProposalPositionGroup.PositionInformation[] positions= curr.getPositions();
if (positions.length > 0) {
LinkedProposalPositionGroup.Proposal[] linkedModeProposals= curr.getProposals();
if (linkedModeProposals.length <= 1) {
for (int i= 0; i < positions.length; i++) {
LinkedProposalPositionGroup.PositionInformation pos= positions[i];
if (pos.getOffset() != -1) {
group.addPosition(new LinkedPosition(document, pos.getOffset(), pos.getLength(), pos.getSequenceRank()));
}
}
} else {
LinkedPositionProposalImpl[] proposalImpls= new LinkedPositionProposalImpl[linkedModeProposals.length];
for (int i= 0; i < linkedModeProposals.length; i++) {
proposalImpls[i]= new LinkedPositionProposalImpl(linkedModeProposals[i], model);
}
for (int i= 0; i < positions.length; i++) {
LinkedProposalPositionGroup.PositionInformation pos= positions[i];
if (pos.getOffset() != -1) {
group.addPosition(new ProposalPosition(document, pos.getOffset(), pos.getLength(), pos.getSequenceRank(), proposalImpls));
}
}
}
model.addGroup(group);
added= true;
}
}
model.forceInstall();
if (editor instanceof JavaEditor) {
model.addLinkingListener(new EditorHighlightingSynchronizer((JavaEditor) editor));
}
if (added) { // only set up UI if there are any positions set
LinkedModeUI ui= new EditorLinkedModeUI(model, viewer);
LinkedProposalPositionGroup.PositionInformation endPosition= fLinkedProposalModel.getEndPosition();
if (endPosition != null && endPosition.getOffset() != -1) {
ui.setExitPosition(viewer, endPosition.getOffset() + endPosition.getLength(), 0, Integer.MAX_VALUE);
} else {
int cursorPosition= viewer.getSelectedRange().x;
if (cursorPosition != 0) {
ui.setExitPosition(viewer, cursorPosition, 0, Integer.MAX_VALUE);
}
}
ui.setExitPolicy(new LinkedModeExitPolicy());
ui.enter();
IRegion region= ui.getSelectedRegion();
viewer.setSelectedRange(region.getOffset(), region.getLength());
viewer.revealRange(region.getOffset(), region.getLength());
}
}
/**
* Creates the text change for this proposal.
* This method is only called once and only when no text change has been passed in
* {@link #CUCorrectionProposal(String, IJavaScriptUnit, TextChange, int, Image)}.
*
* @return returns the created text change.
* @throws CoreException thrown if the creation of the text change failed.
*/
protected TextChange createTextChange() throws CoreException {
IJavaScriptUnit cu= getCompilationUnit();
String name= getName();
TextChange change;
if (!cu.getResource().exists()) {
String source;
try {
source= cu.getSource();
} catch (JavaScriptModelException e) {
JavaScriptPlugin.log(e);
source= new String(); // empty
}
Document document= new Document(source);
document.setInitialLineDelimiter(StubUtility.getLineDelimiterUsed(cu));
change= new DocumentChange(name, document);
} else {
CompilationUnitChange cuChange = new CompilationUnitChange(name, cu);
cuChange.setSaveMode(TextFileChange.LEAVE_DIRTY);
change= cuChange;
}
TextEdit rootEdit= new MultiTextEdit();
change.setEdit(rootEdit);
// initialize text change
IDocument document= change.getCurrentDocument(new NullProgressMonitor());
addEdits(document, rootEdit);
return change;
}
/* (non-Javadoc)
* @see org.eclipse.wst.jsdt.internal.ui.text.correction.ChangeCorrectionProposal#createChange()
*/
protected final Change createChange() throws CoreException {
return createTextChange(); // make sure that only text changes are allowed here
}
/**
* Gets the text change that is invoked when the change is applied.
*
* @return returns the text change that is invoked when the change is applied.
* @throws CoreException throws an exception if accessing the change failed
*/
public final TextChange getTextChange() throws CoreException {
return (TextChange) getChange();
}
/**
* The compilation unit on that the change works.
*
* @return the compilation unit on that the change works.
*/
public final IJavaScriptUnit getCompilationUnit() {
return fCompilationUnit;
}
/**
* Creates a preview of the content of the compilation unit after applying the change.
*
* @return returns the preview of the changed compilation unit.
* @throws CoreException thrown if the creation of the change failed.
*/
public String getPreviewContent() throws CoreException {
return getTextChange().getPreviewContent(new NullProgressMonitor());
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
try {
return getPreviewContent();
} catch (CoreException e) {
}
return super.toString();
}
private static class LinkedModeExitPolicy implements LinkedModeUI.IExitPolicy {
public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
if (event.character == '=') {
return new ExitFlags(ILinkedModeListener.EXIT_ALL, true);
}
return null;
}
}
private static class LinkedPositionProposalImpl implements ICompletionProposalExtension2, IJavaCompletionProposal {
private final LinkedProposalPositionGroup.Proposal fProposal;
private final LinkedModeModel fLinkedPositionModel;
public LinkedPositionProposalImpl(LinkedProposalPositionGroup.Proposal proposal, LinkedModeModel model) {
fProposal= proposal;
fLinkedPositionModel= model;
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#apply(org.eclipse.jface.text.ITextViewer, char, int, int)
*/
public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) {
IDocument doc= viewer.getDocument();
LinkedPosition position= fLinkedPositionModel.findPosition(new LinkedPosition(doc, offset, 0));
if (position != null) {
try {
try {
TextEdit edit= fProposal.computeEdits(offset, position, trigger, stateMask, fLinkedPositionModel);
if (edit != null) {
edit.apply(position.getDocument(), 0);
}
} catch (MalformedTreeException e) {
throw new CoreException(new Status(IStatus.ERROR, JavaScriptUI.ID_PLUGIN, IStatus.ERROR, "Unexpected exception applying edit", e)); //$NON-NLS-1$
} catch (BadLocationException e) {
throw new CoreException(new Status(IStatus.ERROR, JavaScriptUI.ID_PLUGIN, IStatus.ERROR, "Unexpected exception applying edit", e)); //$NON-NLS-1$
}
} catch (CoreException e) {
JavaScriptPlugin.log(e);
}
}
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.contentassist.ICompletionProposal#getDisplayString()
*/
public String getDisplayString() {
return fProposal.getDisplayString();
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.contentassist.ICompletionProposal#getImage()
*/
public Image getImage() {
return fProposal.getImage();
}
/* (non-Javadoc)
* @see org.eclipse.wst.jsdt.ui.text.java.IJavaCompletionProposal#getRelevance()
*/
public int getRelevance() {
return fProposal.getRelevance();
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.contentassist.ICompletionProposal#apply(org.eclipse.jface.text.IDocument)
*/
public void apply(IDocument document) {
// not called
}
/* (non-Javadoc)
* @see org.eclipse.jface.text.contentassist.ICompletionProposal#getAdditionalProposalInfo()
*/
public String getAdditionalProposalInfo() {
return fProposal.getAdditionalProposalInfo();
}
public Point getSelection(IDocument document) { return null; }
public IContextInformation getContextInformation() { return null; }
public void selected(ITextViewer viewer, boolean smartToggle) {}
public void unselected(ITextViewer viewer) {}
/*
* @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument, int, org.eclipse.jface.text.DocumentEvent)
*/
public boolean validate(IDocument document, int offset, DocumentEvent event) {
// ignore event
String insert= getDisplayString();
int off;
LinkedPosition pos= fLinkedPositionModel.findPosition(new LinkedPosition(document, offset, 0));
if (pos != null) {
off= pos.getOffset();
} else {
off= Math.max(0, offset - insert.length());
}
int length= offset - off;
if (offset <= document.getLength()) {
try {
String content= document.get(off, length);
if (insert.startsWith(content))
return true;
} catch (BadLocationException e) {
JavaScriptPlugin.log(e);
// and ignore and return false
}
}
return false;
}
}
}