/******************************************************************************* * Copyright (c) 2014, 2015 Cisco Systems, Inc. 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 * *******************************************************************************/ package com.cisco.yangide.ext.refactoring.ui; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Comparator; import org.eclipse.core.commands.operations.IOperationHistory; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.commands.operations.IUndoableOperation; import org.eclipse.core.commands.operations.OperationHistoryFactory; import org.eclipse.core.resources.IFile; 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.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.ITextViewerExtension6; import org.eclipse.jface.text.IUndoManager; import org.eclipse.jface.text.IUndoManagerExtension; 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.LinkedModeUI.ExitFlags; import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy; import org.eclipse.jface.text.link.LinkedPosition; import org.eclipse.jface.text.link.LinkedPositionGroup; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.SourceViewer; 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 com.cisco.yangide.core.YangModelException; import com.cisco.yangide.core.dom.ASTNamedNode; import com.cisco.yangide.editor.editors.YangEditor; import com.cisco.yangide.ext.refactoring.YangRefactoringPlugin; import com.cisco.yangide.ext.refactoring.actions.RenameSupport; /** * @author Konstantin Zaitsev * @date Aug 4, 2014 */ public class RenameLinkedMode { private class FocusEditingSupport implements IEditingSupport { @Override public boolean ownsFocusShell() { if (fInfoPopup == null) { return false; } if (fInfoPopup.ownsFocusShell()) { return true; } Shell editorShell = editor.getSite().getShell(); Shell activeShell = editorShell.getDisplay().getActiveShell(); if (editorShell == activeShell) { return true; } return false; } @Override public boolean isOriginator(DocumentEvent event, IRegion subjectRegion) { return false; // leave on external modification outside positions } } private class EditorSynchronizer implements ILinkedModeListener { @Override public void left(LinkedModeModel model, int flags) { linkedModeLeft(); if ((flags & ILinkedModeListener.UPDATE_CARET) != 0) { doRename(fShowPreview); } } @Override public void resume(LinkedModeModel model, int flags) { } @Override public void suspend(LinkedModeModel model) { } } private class ExitPolicy implements IExitPolicy { private IDocument document; public ExitPolicy(IDocument document) { this.document = document; } @Override public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) { fShowPreview = (event.stateMask & SWT.CTRL) != 0 && (event.character == SWT.CR || event.character == SWT.LF); if (length == 0 && (event.character == SWT.BS || event.character == SWT.DEL)) { LinkedPosition position = model .findPosition(new LinkedPosition(document, offset, 0, LinkedPositionGroup.NO_STOP)); if (position != null) { if (event.character == SWT.BS) { if (offset - 1 < position.getOffset()) { // skip backspace at beginning of linked position event.doit = false; } } else /* event.character == SWT.DEL */ { if (offset + 1 > position.getOffset() + position.getLength()) { // skip delete at end of linked position event.doit = false; } } } } return null; // don't change behavior } } private static RenameLinkedMode fgActiveLinkedMode; private final YangEditor editor; /** Original definition node */ private final ASTNamedNode originalNode; private RenameInformationPopup fInfoPopup; private Point fOriginalSelection; private String fOriginalName; /** Selected in editor node. */ private ASTNamedNode selectedNode; private LinkedPosition fNamePosition; private LinkedModeModel fLinkedModeModel; private LinkedPositionGroup fLinkedPositionGroup; private final FocusEditingSupport fFocusEditingSupport; private boolean fShowPreview; private IUndoableOperation fStartingUndoOperation; private IFile originalFile; public RenameLinkedMode(ASTNamedNode originalNode, IFile originalFile, ASTNamedNode selectedNode, YangEditor editor) { this.editor = editor; this.originalFile = originalFile; this.originalNode = originalNode; this.selectedNode = selectedNode; fFocusEditingSupport = new FocusEditingSupport(); } public static RenameLinkedMode getActiveLinkedMode() { if (fgActiveLinkedMode != null) { ISourceViewer viewer = fgActiveLinkedMode.editor.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; } ISourceViewer viewer = editor.getViewer(); IDocument document = viewer.getDocument(); fOriginalSelection = viewer.getSelectedRange(); int offset = fOriginalSelection.x; try { if (viewer instanceof ITextViewerExtension6) { IUndoManager undoManager = ((ITextViewerExtension6) viewer).getUndoManager(); if (undoManager instanceof IUndoManagerExtension) { IUndoManagerExtension undoManagerExtension = (IUndoManagerExtension) undoManager; IUndoContext undoContext = undoManagerExtension.getUndoContext(); IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory(); fStartingUndoOperation = operationHistory.getUndoOperation(undoContext); } } fOriginalName = selectedNode.getName(); if (fOriginalName.indexOf(':') > 0) { fOriginalName = fOriginalName.substring(fOriginalName.indexOf(':'), fOriginalName.length()); } final int pos = selectedNode.getNameStartPosition(); fLinkedPositionGroup = new LinkedPositionGroup(); ASTNamedNode[] sameNodes = RenameSupport.findLocalReferences(editor.getModule(), originalNode); Arrays.sort(sameNodes, new Comparator<ASTNamedNode>() { @Override public int compare(ASTNamedNode o1, ASTNamedNode o2) { return rank(o1) - rank(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(ASTNamedNode node) { int relativeRank = node.getNameStartPosition() + node.getNameLength() - pos; if (relativeRank < 0) { return Integer.MAX_VALUE + relativeRank; } else { return relativeRank; } } }); for (int i = 0; i < sameNodes.length; i++) { ASTNamedNode elem = sameNodes[i]; String name = elem.getName(); int elPrefixOffset = name.indexOf(':'); int elPos = elem.getNameStartPosition(); int elLength = elem.getNameLength(); // normalize quote if (name.length() + 2 == elLength) { elPos++; elLength -= 2; } // normalize prefixes if (elPrefixOffset > 0) { elPos += elPrefixOffset + 1; elLength = elLength - elPrefixOffset - 1; } LinkedPosition linkedPosition = new LinkedPosition(document, elPos, elLength, i); if (i == 0) { fNamePosition = linkedPosition; } fLinkedPositionGroup.addPosition(linkedPosition); } fLinkedModeModel = new LinkedModeModel(); fLinkedModeModel.addGroup(fLinkedPositionGroup); fLinkedModeModel.forceInstall(); fLinkedModeModel.addLinkingListener(new EditorHighlightingSynchronizer(editor)); 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(); // by default, full word is selected; // restore original selection viewer.setSelectedRange(fOriginalSelection.x, fOriginalSelection.y); if (viewer instanceof IEditingSupportRegistry) { IEditingSupportRegistry registry = (IEditingSupportRegistry) viewer; registry.register(fFocusEditingSupport); } openSecondaryPopup(); // startAnimation(); fgActiveLinkedMode = this; } catch (BadLocationException | YangModelException e) { YangRefactoringPlugin.log(e); } } void doRename(boolean showPreview) { cancel(); Image image = null; Label label = null; fShowPreview |= showPreview; try { ISourceViewer viewer = editor.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 = editor.getSite().getShell(); boolean executed; if (fShowPreview) { executed = renameSupport.openDialog(shell, true); } else { renameSupport.perform(shell, editor.getSite().getWorkbenchWindow()); executed = true; } if (executed) { restoreFullSelection(); } editor.reconcileModel(); } catch (BadLocationException | CoreException e) { YangRefactoringPlugin.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)) { editor.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. final ISourceViewer viewer = editor.getViewer(); try { if (!fOriginalName.equals(newName)) { editor.getSite().getWorkbenchWindow().run(false, true, new IRunnableWithProgress() { @Override public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { if (viewer instanceof ITextViewerExtension6) { IUndoManager undoManager = ((ITextViewerExtension6) viewer).getUndoManager(); if (undoManager instanceof IUndoManagerExtension) { IUndoManagerExtension undoManagerExtension = (IUndoManagerExtension) undoManager; IUndoContext undoContext = undoManagerExtension.getUndoContext(); IOperationHistory operationHistory = OperationHistoryFactory.getOperationHistory(); while (undoManager.undoable()) { if (fStartingUndoOperation != null && fStartingUndoOperation .equals(operationHistory.getUndoOperation(undoContext))) { return; } undoManager.undo(); } } } } }); } } catch (InvocationTargetException e) { throw new CoreException( new Status(IStatus.ERROR, YangRefactoringPlugin.PLUGIN_ID, "Error saving editor", e)); } catch (InterruptedException e) { // canceling is OK return null; } finally { editor.reconcileModel(); } viewer.setSelectedRange(fOriginalSelection.x, fOriginalSelection.y); if (newName.length() == 0) { return null; } return new RenameSupport(originalFile, originalNode, newName); } public void startFullDialog() { cancel(); try { String newName = fNamePosition.getContent(); RenameSupport renameSupport = undoAndCreateRenameSupport(newName); if (renameSupport != null) { renameSupport.openDialog(editor.getSite().getShell()); } } catch (BadLocationException | CoreException e) { YangRefactoringPlugin.log(e); } } private void linkedModeLeft() { fgActiveLinkedMode = null; if (fInfoPopup != null) { fInfoPopup.close(); } ISourceViewer viewer = editor.getViewer(); if (viewer instanceof IEditingSupportRegistry) { IEditingSupportRegistry registry = (IEditingSupportRegistry) viewer; registry.unregister(fFocusEditingSupport); } } private void openSecondaryPopup() { fInfoPopup = new RenameInformationPopup(editor, this); fInfoPopup.open(); } public boolean isCaretInLinkedPosition() { return getCurrentLinkedPosition() != null; } public LinkedPosition getCurrentLinkedPosition() { Point selection = editor.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 isOriginalName() { try { String newName = fNamePosition.getContent(); return fOriginalName.equals(newName); } catch (BadLocationException e) { return false; } } }