blob: b4397c313425613601fbedc0bd905d863bd1d056 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 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.refactoring.reorg;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Comparator;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IEditingSupport;
import org.eclipse.jface.text.IEditingSupportRegistry;
import org.eclipse.jface.text.IRegion;
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.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.ltk.core.refactoring.RefactoringCore;
import org.eclipse.ltk.ui.refactoring.RefactoringWizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
import org.eclipse.wst.jsdt.core.IField;
import org.eclipse.wst.jsdt.core.IJavaScriptElement;
import org.eclipse.wst.jsdt.core.IJavaScriptProject;
import org.eclipse.wst.jsdt.core.IFunction;
import org.eclipse.wst.jsdt.core.JavaScriptConventions;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.core.JavaScriptModelException;
import org.eclipse.wst.jsdt.core.dom.ASTNode;
import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit;
import org.eclipse.wst.jsdt.core.dom.SimpleName;
import org.eclipse.wst.jsdt.core.refactoring.IJavaScriptRefactorings;
import org.eclipse.wst.jsdt.core.refactoring.descriptors.RenameJavaScriptElementDescriptor;
import org.eclipse.wst.jsdt.internal.corext.dom.LinkedNodeFinder;
import org.eclipse.wst.jsdt.internal.corext.dom.NodeFinder;
import org.eclipse.wst.jsdt.internal.corext.refactoring.rename.RenamingNameSuggestor;
import org.eclipse.wst.jsdt.internal.corext.util.JavaModelUtil;
import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.ASTProvider;
import org.eclipse.wst.jsdt.internal.ui.javaeditor.CompilationUnitEditor;
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.refactoring.DelegateUIHelper;
import org.eclipse.wst.jsdt.internal.ui.text.correction.LinkedNamesAssistProposal.DeleteBlockingExitPolicy;
import org.eclipse.wst.jsdt.ui.refactoring.RenameSupport;
public class RenameLinkedMode {
private class FocusEditingSupport implements IEditingSupport {
public boolean ownsFocusShell() {
if (fInfoPopup == null)
return false;
if (fInfoPopup.ownsFocusShell()) {
return true;
}
Shell editorShell= fEditor.getSite().getShell();
Shell activeShell= editorShell.getDisplay().getActiveShell();
if (editorShell == activeShell)
return true;
return false;
}
public boolean isOriginator(DocumentEvent event, IRegion subjectRegion) {
return false; //leave on external modification outside positions
}
}
private class EditorSynchronizer implements ILinkedModeListener {
public void left(LinkedModeModel model, int flags) {
linkedModeLeft();
if ( (flags & ILinkedModeListener.UPDATE_CARET) != 0) {
doRename(fShowPreview);
}
}
public void resume(LinkedModeModel model, int flags) {
}
public void suspend(LinkedModeModel model) {
}
}
private class ExitPolicy extends DeleteBlockingExitPolicy {
public ExitPolicy(IDocument document) {
super(document);
}
public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
fShowPreview|= (event.stateMask & SWT.CTRL) != 0;
return super.doExit(model, event, offset, length);
}
}
private static RenameLinkedMode fgActiveLinkedMode;
private final CompilationUnitEditor fEditor;
private final IJavaScriptElement fJavaElement;
private RenameInformationPopup fInfoPopup;
private boolean fOriginalSaved;
private Point fOriginalSelection;
private String fOriginalName;
private LinkedPosition fNamePosition;
private LinkedModeModel fLinkedModeModel;
private LinkedPositionGroup fLinkedPositionGroup;
private final FocusEditingSupport fFocusEditingSupport;
private boolean fShowPreview;
public RenameLinkedMode(IJavaScriptElement element, CompilationUnitEditor editor) {
Assert.isNotNull(element);
Assert.isNotNull(editor);
fEditor= editor;
fJavaElement= element;
fFocusEditingSupport= new FocusEditingSupport();
}
public static RenameLinkedMode getActiveLinkedMode() {
if (fgActiveLinkedMode != null) {
ISourceViewer viewer= fgActiveLinkedMode.fEditor.getViewer();
if (viewer != null) {
StyledText textWidget= viewer.getTextWidget();
if (textWidget != null && ! textWidget.isDisposed()) {
return fgActiveLinkedMode;
}
}
// make sure we don't hold onto the active linked mode if anything went wrong with canceling:
fgActiveLinkedMode= null;
}
return null;
}
public void start() {
if (getActiveLinkedMode() != null) {
// for safety; should already be handled in RenameJavaElementAction
fgActiveLinkedMode.startFullDialog();
return;
}
fOriginalSaved= ! fEditor.isDirty();
ISourceViewer viewer= fEditor.getViewer();
IDocument document= viewer.getDocument();
fOriginalSelection= viewer.getSelectedRange();
int offset= fOriginalSelection.x;
try {
JavaScriptUnit root= JavaScriptPlugin.getDefault().getASTProvider().getAST(getCompilationUnit(), ASTProvider.WAIT_YES, null);
fLinkedPositionGroup= new LinkedPositionGroup();
ASTNode selectedNode= NodeFinder.perform(root, fOriginalSelection.x, fOriginalSelection.y);
if (! (selectedNode instanceof SimpleName)) {
return; // TODO: show dialog
}
SimpleName nameNode= (SimpleName) selectedNode;
fOriginalName= nameNode.getIdentifier();
final int pos= nameNode.getStartPosition();
ASTNode[] sameNodes= LinkedNodeFinder.findByNode(root, nameNode);
//TODO: copied from LinkedNamesAssistProposal#apply(..):
// sort for iteration order, starting with the node @ offset
Arrays.sort(sameNodes, new Comparator() {
public int compare(Object o1, Object o2) {
return rank((ASTNode) o1) - rank((ASTNode) o2);
}
/**
* Returns the absolute rank of an <code>ASTNode</code>. Nodes
* preceding <code>pos</code> are ranked last.
*
* @param node the node to compute the rank for
* @return the rank of the node with respect to the invocation offset
*/
private int rank(ASTNode node) {
int relativeRank= node.getStartPosition() + node.getLength() - pos;
if (relativeRank < 0)
return Integer.MAX_VALUE + relativeRank;
else
return relativeRank;
}
});
for (int i= 0; i < sameNodes.length; i++) {
ASTNode elem= sameNodes[i];
LinkedPosition linkedPosition= new LinkedPosition(document, elem.getStartPosition(), elem.getLength(), i);
if (i == 0)
fNamePosition= linkedPosition;
fLinkedPositionGroup.addPosition(linkedPosition);
}
fLinkedModeModel= new LinkedModeModel();
fLinkedModeModel.addGroup(fLinkedPositionGroup);
fLinkedModeModel.forceInstall();
fLinkedModeModel.addLinkingListener(new EditorHighlightingSynchronizer(fEditor));
fLinkedModeModel.addLinkingListener(new EditorSynchronizer());
LinkedModeUI ui= new EditorLinkedModeUI(fLinkedModeModel, viewer);
ui.setExitPosition(viewer, offset, 0, Integer.MAX_VALUE);
ui.setExitPolicy(new ExitPolicy(document));
ui.enter();
viewer.setSelectedRange(fOriginalSelection.x, fOriginalSelection.y); // by default, full word is selected; restore original selection
if (viewer instanceof IEditingSupportRegistry) {
IEditingSupportRegistry registry= (IEditingSupportRegistry) viewer;
registry.register(fFocusEditingSupport);
}
openSecondaryPopup();
// startAnimation();
fgActiveLinkedMode= this;
} catch (BadLocationException e) {
JavaScriptPlugin.log(e);
}
}
// private void startAnimation() {
// //TODO:
// // - switch off if animations disabled
// // - show rectangle around target for 500ms after animation
// Shell shell= fEditor.getSite().getShell();
// StyledText textWidget= fEditor.getViewer().getTextWidget();
//
// // from popup:
// Rectangle startRect= fPopup.getBounds();
//
// // from editor:
//// Point startLoc= textWidget.getParent().toDisplay(textWidget.getLocation());
//// Point startSize= textWidget.getSize();
//// Rectangle startRect= new Rectangle(startLoc.x, startLoc.y, startSize.x, startSize.y);
//
// // from hell:
//// Rectangle startRect= shell.getClientArea();
//
// Point caretLocation= textWidget.getLocationAtOffset(textWidget.getCaretOffset());
// Point displayLocation= textWidget.toDisplay(caretLocation);
// Rectangle targetRect= new Rectangle(displayLocation.x, displayLocation.y, 0, 0);
//
// RectangleAnimation anim= new RectangleAnimation(shell, startRect, targetRect);
// anim.schedule();
// }
void doRename(boolean showPreview) {
cancel();
Image image= null;
Label label= null;
fShowPreview|= showPreview;
try {
ISourceViewer viewer= fEditor.getViewer();
if (viewer instanceof SourceViewer) {
SourceViewer sourceViewer= (SourceViewer) viewer;
Control viewerControl= sourceViewer.getControl();
if (viewerControl instanceof Composite) {
Composite composite= (Composite) viewerControl;
Display display= composite.getDisplay();
// Flush pending redraw requests:
while (! display.isDisposed() && display.readAndDispatch()) {
}
// Copy editor area:
GC gc= new GC(composite);
Point size;
try {
size= composite.getSize();
image= new Image(gc.getDevice(), size.x, size.y);
gc.copyArea(image, 0, 0);
} finally {
gc.dispose();
gc= null;
}
// Persist editor area while executing refactoring:
label= new Label(composite, SWT.NONE);
label.setImage(image);
label.setBounds(0, 0, size.x, size.y);
label.moveAbove(null);
}
}
String newName= fNamePosition.getContent();
if (fOriginalName.equals(newName))
return;
RenameSupport renameSupport= undoAndCreateRenameSupport(newName);
if (renameSupport == null)
return;
Shell shell= fEditor.getSite().getShell();
boolean executed;
if (fShowPreview) { // could have been updated by undoAndCreateRenameSupport(..)
executed= renameSupport.openDialog(shell, true);
} else {
renameSupport.perform(shell, fEditor.getSite().getWorkbenchWindow());
executed= true;
}
if (executed) {
restoreFullSelection();
}
JavaModelUtil.reconcile(getCompilationUnit());
} catch (CoreException ex) {
JavaScriptPlugin.log(ex);
} catch (InterruptedException ex) {
// canceling is OK -> redo text changes in that case?
} catch (InvocationTargetException ex) {
JavaScriptPlugin.log(ex);
} catch (BadLocationException e) {
JavaScriptPlugin.log(e);
} finally {
if (label != null)
label.dispose();
if (image != null)
image.dispose();
}
}
public void cancel() {
if (fLinkedModeModel != null) {
fLinkedModeModel.exit(ILinkedModeListener.NONE);
}
linkedModeLeft();
}
private void restoreFullSelection() {
if (fOriginalSelection.y != 0) {
int originalOffset= fOriginalSelection.x;
LinkedPosition[] positions= fLinkedPositionGroup.getPositions();
for (int i= 0; i < positions.length; i++) {
LinkedPosition position= positions[i];
if (! position.isDeleted() && position.includes(originalOffset)) {
fEditor.getViewer().setSelectedRange(position.offset, position.length);
return;
}
}
}
}
private RenameSupport undoAndCreateRenameSupport(String newName) throws CoreException {
// Assumption: the linked mode model should be shut down by now.
ISourceViewer viewer= fEditor.getViewer();
final IDocument document= viewer.getDocument();
try {
if (! fOriginalName.equals(newName)) {
fEditor.getSite().getWorkbenchWindow().run(false, true, new IRunnableWithProgress() {
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
LinkedPosition[] positions= fLinkedPositionGroup.getPositions();
Arrays.sort(positions, new Comparator() {
public int compare(Object o1, Object o2) {
return ((LinkedPosition) o1).offset - ((LinkedPosition) o2).offset;
}
});
int correction= 0;
int originalLength= fOriginalName.length();
for (int i= 0; i < positions.length; i++) {
LinkedPosition position= positions[i];
try {
int length= position.getLength();
document.replace(position.getOffset() + correction, length, fOriginalName);
correction= correction - length + originalLength;
} catch (BadLocationException e) {
throw new InvocationTargetException(e);
}
}
if (fOriginalSaved) {
fEditor.doSave(monitor); // started saved -> end saved
}
}
});
}
} catch (InvocationTargetException e) {
throw new CoreException(new Status(IStatus.ERROR, JavaScriptPlugin.getPluginId(), ReorgMessages.RenameLinkedMode_error_saving_editor, e));
} catch (InterruptedException e) {
// cancelling is OK
return null;
} finally {
JavaModelUtil.reconcile(getCompilationUnit());
}
viewer.setSelectedRange(fOriginalSelection.x, fOriginalSelection.y);
RenameJavaScriptElementDescriptor descriptor= createRenameDescriptor(fJavaElement, newName);
RenameSupport renameSupport= RenameSupport.create(descriptor);
return renameSupport;
}
private IJavaScriptUnit getCompilationUnit() {
return (IJavaScriptUnit) EditorUtility.getEditorInputJavaElement(fEditor, false);
}
public void startFullDialog() {
cancel();
try {
String newName= fNamePosition.getContent();
RenameSupport renameSupport= undoAndCreateRenameSupport(newName);
if (renameSupport != null)
renameSupport.openDialog(fEditor.getSite().getShell());
} catch (CoreException e) {
JavaScriptPlugin.log(e);
} catch (BadLocationException e) {
JavaScriptPlugin.log(e);
}
}
/**
* @param javaElement
* @param newName
* @return a rename descriptor with current settings as used in the refactoring dialogs
* @throws JavaScriptModelException
*/
private RenameJavaScriptElementDescriptor createRenameDescriptor(IJavaScriptElement javaElement, String newName) throws JavaScriptModelException {
String contributionId;
// see RefactoringExecutionStarter#createRenameSupport(..):
int elementType= javaElement.getElementType();
switch (elementType) {
case IJavaScriptElement.JAVASCRIPT_PROJECT:
contributionId= IJavaScriptRefactorings.RENAME_JAVA_PROJECT;
break;
case IJavaScriptElement.PACKAGE_FRAGMENT_ROOT:
contributionId= IJavaScriptRefactorings.RENAME_SOURCE_FOLDER;
break;
case IJavaScriptElement.PACKAGE_FRAGMENT:
contributionId= IJavaScriptRefactorings.RENAME_PACKAGE;
break;
case IJavaScriptElement.JAVASCRIPT_UNIT:
contributionId= IJavaScriptRefactorings.RENAME_JAVASCRIPT_UNIT;
break;
case IJavaScriptElement.TYPE:
contributionId= IJavaScriptRefactorings.RENAME_TYPE;
break;
case IJavaScriptElement.METHOD:
final IFunction method= (IFunction) javaElement;
if (method.isConstructor())
return createRenameDescriptor(method.getDeclaringType(), newName);
else
contributionId= IJavaScriptRefactorings.RENAME_METHOD;
break;
case IJavaScriptElement.FIELD:
IField field= (IField) javaElement;
if (field.isEnumConstant())
contributionId= IJavaScriptRefactorings.RENAME_ENUM_CONSTANT;
else
contributionId= IJavaScriptRefactorings.RENAME_FIELD;
break;
case IJavaScriptElement.TYPE_PARAMETER:
contributionId= IJavaScriptRefactorings.RENAME_TYPE_PARAMETER;
break;
case IJavaScriptElement.LOCAL_VARIABLE:
contributionId= IJavaScriptRefactorings.RENAME_LOCAL_VARIABLE;
break;
default:
return null;
}
RenameJavaScriptElementDescriptor descriptor= (RenameJavaScriptElementDescriptor) RefactoringCore.getRefactoringContribution(contributionId).createDescriptor();
descriptor.setJavaElement(javaElement);
descriptor.setNewName(newName);
if (elementType != IJavaScriptElement.PACKAGE_FRAGMENT_ROOT)
descriptor.setUpdateReferences(true);
IDialogSettings javaSettings= JavaScriptPlugin.getDefault().getDialogSettings();
IDialogSettings refactoringSettings= javaSettings.getSection(RefactoringWizardPage.REFACTORING_SETTINGS); //TODO: undocumented API
if (refactoringSettings == null) {
refactoringSettings= javaSettings.addNewSection(RefactoringWizardPage.REFACTORING_SETTINGS);
}
switch (elementType) {
case IJavaScriptElement.METHOD:
case IJavaScriptElement.FIELD:
descriptor.setDeprecateDelegate(refactoringSettings.getBoolean(DelegateUIHelper.DELEGATE_DEPRECATION));
descriptor.setKeepOriginal(refactoringSettings.getBoolean(DelegateUIHelper.DELEGATE_UPDATING));
}
switch (elementType) {
case IJavaScriptElement.TYPE:
// case IJavaScriptElement.JAVASCRIPT_UNIT: // TODO
descriptor.setUpdateSimilarDeclarations(refactoringSettings.getBoolean(RenameRefactoringWizard.TYPE_UPDATE_SIMILAR_ELEMENTS));
int strategy;
try {
strategy= refactoringSettings.getInt(RenameRefactoringWizard.TYPE_SIMILAR_MATCH_STRATEGY);
} catch (NumberFormatException e) {
strategy= RenamingNameSuggestor.STRATEGY_EXACT;
}
descriptor.setMatchStrategy(strategy);
}
switch (elementType) {
case IJavaScriptElement.PACKAGE_FRAGMENT:
descriptor.setUpdateHierarchy(refactoringSettings.getBoolean(RenameRefactoringWizard.PACKAGE_RENAME_SUBPACKAGES));
}
switch (elementType) {
case IJavaScriptElement.PACKAGE_FRAGMENT:
case IJavaScriptElement.TYPE:
String fileNamePatterns= refactoringSettings.get(RenameRefactoringWizard.QUALIFIED_NAMES_PATTERNS);
if (fileNamePatterns != null && fileNamePatterns.length() != 0) {
descriptor.setFileNamePatterns(fileNamePatterns);
boolean updateQualifiedNames= refactoringSettings.getBoolean(RenameRefactoringWizard.UPDATE_QUALIFIED_NAMES);
descriptor.setUpdateQualifiedNames(updateQualifiedNames);
fShowPreview|= updateQualifiedNames;
}
}
switch (elementType) {
case IJavaScriptElement.PACKAGE_FRAGMENT:
case IJavaScriptElement.TYPE:
case IJavaScriptElement.FIELD:
boolean updateTextualOccurrences= refactoringSettings.getBoolean(RenameRefactoringWizard.UPDATE_TEXTUAL_MATCHES);
descriptor.setUpdateTextualOccurrences(updateTextualOccurrences);
fShowPreview|= updateTextualOccurrences;
}
switch (elementType) {
case IJavaScriptElement.FIELD:
descriptor.setRenameGetters(refactoringSettings.getBoolean(RenameRefactoringWizard.FIELD_RENAME_GETTER));
descriptor.setRenameSetters(refactoringSettings.getBoolean(RenameRefactoringWizard.FIELD_RENAME_SETTER));
}
return descriptor;
}
private void linkedModeLeft() {
fgActiveLinkedMode= null;
if (fInfoPopup != null) {
fInfoPopup.close();
}
ISourceViewer viewer= fEditor.getViewer();
if (viewer instanceof IEditingSupportRegistry) {
IEditingSupportRegistry registry= (IEditingSupportRegistry) viewer;
registry.unregister(fFocusEditingSupport);
}
}
private void openSecondaryPopup() {
fInfoPopup= new RenameInformationPopup(fEditor, this);
fInfoPopup.open();
}
public boolean isCaretInLinkedPosition() {
return getCurrentLinkedPosition() != null;
}
public LinkedPosition getCurrentLinkedPosition() {
Point selection= fEditor.getViewer().getSelectedRange();
int start= selection.x;
int end= start + selection.y;
LinkedPosition[] positions= fLinkedPositionGroup.getPositions();
for (int i= 0; i < positions.length; i++) {
LinkedPosition position= positions[i];
if (position.includes(start) && position.includes(end))
return position;
}
return null;
}
public boolean isEnabled() {
try {
String newName= fNamePosition.getContent();
if (fOriginalName.equals(newName))
return false;
/*
* TODO: use JavaRenameProcessor#checkNewElementName(String)
* but make sure implementations don't access outdated Java Model
* (cache all necessary information before starting linked mode).
*/
IJavaScriptProject project= fJavaElement.getJavaScriptProject();
String sourceLevel= project.getOption(JavaScriptCore.COMPILER_SOURCE, true);
String complianceLevel= project.getOption(JavaScriptCore.COMPILER_COMPLIANCE, true);
return JavaScriptConventions.validateIdentifier(newName, sourceLevel, complianceLevel).isOK();
} catch (BadLocationException e) {
return false;
}
}
public boolean isOriginalName() {
try {
String newName= fNamePosition.getContent();
return fOriginalName.equals(newName);
} catch (BadLocationException e) {
return false;
}
}
}