/* * Copyright (c) 2012, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.ui.internal.text.editor; import com.google.dart.tools.core.DartCoreDebug; import com.google.dart.tools.internal.corext.refactoring.util.ReflectionUtils; import com.google.dart.tools.ui.internal.text.dart.DartAutoIndentStrategy_NEW; import com.google.dart.tools.ui.internal.text.dart.DartAutoIndentStrategy_OLD; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.IRewriteTarget; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITextViewerExtension5; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.ILineRange; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.LineRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Event; import org.eclipse.ui.texteditor.IEditorStatusLine; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; import org.eclipse.ui.texteditor.TextEditorAction; import java.util.ResourceBundle; /** * Action for moving selected lines in a Java editor. */ public class DartMoveLinesAction extends TextEditorAction { /** * State shared by the Move / Copy lines action quadruple. */ private static final class SharedState { /** The compilation unit editor that all four actions operate on. */ public CompilationUnitEditor fEditor; // /** // * The indent token shared by all four actions. // */ // public IndentResult fResult = null; /** * Set to true before modifying the document, to false after. */ // boolean fIsChanging = false; /** <code>true</code> if a compound move / copy is going on. */ private boolean fEditInProgress = false; /** The exit strategy that will detect the ending of a compound edit */ private final CompoundEditExitStrategy fExitStrategy; public SharedState(CompilationUnitEditor editor) { fEditor = editor; fExitStrategy = new CompoundEditExitStrategy(new String[] { ITextEditorActionDefinitionIds.MOVE_LINES_UP, ITextEditorActionDefinitionIds.MOVE_LINES_DOWN, ITextEditorActionDefinitionIds.COPY_LINES_UP, ITextEditorActionDefinitionIds.COPY_LINES_DOWN}); fExitStrategy.addCompoundListener(new ICompoundEditListener() { @Override public void endCompoundEdit() { SharedState.this.endCompoundEdit(); } }); } /** * Ends the compound change. */ public void beginCompoundEdit() { if (fEditInProgress || fEditor == null) { return; } fEditInProgress = true; fExitStrategy.arm(fEditor.getViewer()); IRewriteTarget target = (IRewriteTarget) fEditor.getAdapter(IRewriteTarget.class); if (target != null) { target.beginCompoundChange(); } } /** * Ends the compound change. */ public void endCompoundEdit() { if (!fEditInProgress || fEditor == null) { return; } fExitStrategy.disarm(); IRewriteTarget target = (IRewriteTarget) fEditor.getAdapter(IRewriteTarget.class); if (target != null) { target.endCompoundChange(); } // fResult = null; fEditInProgress = false; } } /* keys */ /** * Key for status message upon illegal move. * <p> * Value {@value} * </p> */ /* state variables - define what this action does */ /** * Creates the quadruple of move and copy actions. The returned array contains the actions in the * following order: [0] move up [1] move down [2] copy up (duplicate) [3] copy down (duplicate & * select) * * @param bundle the resource bundle * @param editor the editor * @return the quadruple of actions */ public static DartMoveLinesAction[] createMoveCopyActionSet(ResourceBundle bundle, CompilationUnitEditor editor) { SharedState state = new SharedState(editor); DartMoveLinesAction[] actions = new DartMoveLinesAction[4]; actions[0] = new DartMoveLinesAction(bundle, "Editor.MoveLinesUp.", true, false, state); //$NON-NLS-1$ actions[1] = new DartMoveLinesAction(bundle, "Editor.MoveLinesDown.", false, false, state); //$NON-NLS-1$ actions[2] = new DartMoveLinesAction(bundle, "Editor.CopyLineUp.", true, true, state); //$NON-NLS-1$ actions[3] = new DartMoveLinesAction(bundle, "Editor.CopyLineDown.", false, true, state); //$NON-NLS-1$ return actions; } /** * <code>true</code> if lines are shifted upwards, <code>false</code> otherwise. */ private final boolean fUpwards; /** <code>true</code> if lines are to be copied instead of moved. */ private final boolean fCopy; /** The shared state of the move/copy action quadruple. */ private final SharedState fSharedState; /** * Creates and initializes the action for the given text editor. The action configures its visual * representation from the given resource bundle. * * @param bundle the resource bundle * @param prefix a prefix to be prepended to the various resource keys (described in * <code>ResourceAction</code> constructor), or <code>null</code> if none * @param upwards <code>true</code>if the selected lines should be moved upwards, * <code>false</code> if downwards * @param copy if <code>true</code>, the action will copy lines instead of moving them * @param state the shared state * @see TextEditorAction#TextEditorAction(ResourceBundle, String, ITextEditor) */ private DartMoveLinesAction(ResourceBundle bundle, String prefix, boolean upwards, boolean copy, SharedState state) { super(bundle, prefix, state.fEditor); fUpwards = upwards; fCopy = copy; fSharedState = state; update(); } /* * @see org.eclipse.jface.action.IAction#run() */ @Override public void runWithEvent(Event event) { // get involved objects if (fSharedState.fEditor == null) { return; } if (!validateEditorInputState()) { return; } ISourceViewer viewer = fSharedState.fEditor.getViewer(); if (viewer == null) { return; } IDocument document = viewer.getDocument(); if (document == null) { return; } StyledText widget = viewer.getTextWidget(); if (widget == null) { return; } // get selection ITextSelection sel = (ITextSelection) viewer.getSelectionProvider().getSelection(); if (sel.isEmpty()) { return; } ITextSelection skippedLine = getSkippedLine(document, sel); if (skippedLine == null) { return; } try { ITextSelection movingArea = getMovingSelection(document, sel, viewer); // if either the skipped line or the moving lines are outside the widget's // visible area, bail out if (!containedByVisibleRegion(movingArea, viewer) || !containedByVisibleRegion(skippedLine, viewer)) { return; } // get the content to be moved around: the moving (selected) area and the skipped line String moving = movingArea.getText(); String skipped = skippedLine.getText(); if (moving == null || skipped == null || document.getLength() == 0) { return; } // prepare selections ILineRange selectionBefore = getLineRange(document, movingArea); ILineRange selectionAfter; fSharedState.beginCompoundEdit(); String delim; if (fUpwards) { delim = document.getLineDelimiter(skippedLine.getEndLine()); if (delim == null) { delim = TextUtilities.getDefaultLineDelimiter(document); } if (fCopy) { int targetOffset = movingArea.getOffset(); DocumentCommand command = customizePasteCommand(viewer, document, moving, targetOffset); command.text += delim; executeCommand(document, command); selectionAfter = selectionBefore; } else { int targetOffset = skippedLine.getOffset(); DocumentCommand command = customizePasteCommand(viewer, document, moving, targetOffset); command.text += delim + skipped; command.length = moving.length() + delim.length() + skipped.length(); executeCommand(document, command); selectionAfter = new LineRange( selectionBefore.getStartLine() - 1, selectionBefore.getNumberOfLines()); } } else { delim = document.getLineDelimiter(movingArea.getEndLine()); if (delim == null) { delim = TextUtilities.getDefaultLineDelimiter(document); } if (fCopy) { int targetOffset = movingArea.getOffset() + movingArea.getLength() + delim.length(); DocumentCommand command = customizePasteCommand(viewer, document, moving, targetOffset); command.text += delim; executeCommand(document, command); selectionAfter = new LineRange(selectionBefore.getStartLine() + selectionBefore.getNumberOfLines(), selectionBefore.getNumberOfLines()); } else { int targetOffset = skippedLine.getOffset() + skippedLine.getLength() + delim.length(); DocumentCommand command = customizePasteCommand(viewer, document, moving, targetOffset); command.offset = movingArea.getOffset(); command.length = moving.length() + delim.length() + skipped.length(); command.text = skipped + delim + command.text; executeCommand(document, command); selectionAfter = new LineRange( selectionBefore.getStartLine() + 1, selectionBefore.getNumberOfLines()); } } // move the selection along IRegion region = getRegion(document, selectionAfter); selectAndReveal(viewer, region.getOffset(), region.getLength()); } catch (BadLocationException x) { // won't happen without concurrent modification - bail out return; } finally { if (fCopy) { fSharedState.endCompoundEdit(); } } } /* * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor * .ITextEditor) */ @Override public void setEditor(ITextEditor editor) { Assert.isTrue(editor instanceof CompilationUnitEditor); super.setEditor(editor); if (fSharedState != null) { fSharedState.fEditor = (CompilationUnitEditor) editor; } } /* * @see org.eclipse.ui.texteditor.IUpdate#update() */ @Override public void update() { super.update(); if (isEnabled()) { setEnabled(canModifyEditor()); } } /** * Checks if <code>selection</code> is contained by the visible region of <code>viewer</code>. As * a special case, a selection is considered contained even if it extends over the visible region, * but the extension stays on a partially contained line and contains only white space. * * @param selection the selection to be checked * @param viewer the viewer displaying a visible region of <code>selection</code>'s document. * @return <code>true</code>, if <code>selection</code> is contained, <code>false</code> * otherwise. */ private boolean containedByVisibleRegion(ITextSelection selection, ISourceViewer viewer) { int min = selection.getOffset(); int max = min + selection.getLength(); IDocument document = viewer.getDocument(); IRegion visible; if (viewer instanceof ITextViewerExtension5) { visible = ((ITextViewerExtension5) viewer).getModelCoverage(); } else { visible = viewer.getVisibleRegion(); } int visOffset = visible.getOffset(); try { if (visOffset > min) { if (document.getLineOfOffset(visOffset) != selection.getStartLine()) { return false; } if (!isWhitespace(document.get(min, visOffset - min))) { showStatus(); return false; } } int visEnd = visOffset + visible.getLength(); if (visEnd < max) { if (document.getLineOfOffset(visEnd) != selection.getEndLine()) { return false; } if (!isWhitespace(document.get(visEnd, max - visEnd))) { showStatus(); return false; } } return true; } catch (BadLocationException e) { } return false; } private DocumentCommand customizePasteCommand(ISourceViewer viewer, IDocument document, String moving, int offset2) { DocumentCommand command = new DocumentCommand() { }; command.caretOffset = -1; command.doit = true; command.offset = offset2; command.text = moving; if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { DartAutoIndentStrategy_NEW strategy = new DartAutoIndentStrategy_NEW(null, viewer); strategy.customizeDocumentCommand(document, command); } else { DartAutoIndentStrategy_OLD strategy = new DartAutoIndentStrategy_OLD(null, viewer); strategy.customizeDocumentCommand(document, command); } return command; } private void executeCommand(IDocument document, DocumentCommand command) { ReflectionUtils.invokeMethod(command, "execute(org.eclipse.jface.text.IDocument)", document); } private ILineRange getLineRange(IDocument document, ITextSelection selection) throws BadLocationException { final int offset = selection.getOffset(); int startLine = document.getLineOfOffset(offset); int endOffset = offset + selection.getLength(); int endLine = document.getLineOfOffset(endOffset); final int nLines = endLine - startLine + 1; return new LineRange(startLine, nLines); } /** * Given a selection on a document, computes the lines fully or partially covered by * <code>selection</code>. A line in the document is considered covered if <code>selection</code> * comprises any characters on it, including the terminating delimiter. * <p> * Note that the last line in a selection is not considered covered if the selection only * comprises the line delimiter at its beginning (that is considered part of the second last * line). As a special case, if the selection is empty, a line is considered covered if the caret * is at any position in the line, including between the delimiter and the start of the line. The * line containing the delimiter is not considered covered in that case. * </p> * * @param document the document <code>selection</code> refers to * @param selection a selection on <code>document</code> * @param viewer the <code>ISourceViewer</code> displaying <code>document</code> * @return a selection describing the range of lines (partially) covered by <code>selection</code> * , without any terminating line delimiters * @throws BadLocationException if the selection is out of bounds (when the underlying document * has changed during the call) */ private ITextSelection getMovingSelection(IDocument document, ITextSelection selection, ISourceViewer viewer) throws BadLocationException { int low = document.getLineOffset(selection.getStartLine()); int endLine = selection.getEndLine(); int high = document.getLineOffset(endLine) + document.getLineLength(endLine); // get everything up to last line without its delimiter String delim = document.getLineDelimiter(endLine); if (delim != null) { high -= delim.length(); } return new TextSelection(document, low, high - low); } // private DartProject getProject() { // IEditorInput editorInput = fSharedState.fEditor.getEditorInput(); // CompilationUnit unit = DartToolsPlugin.getDefault().getWorkingCopyManager().getWorkingCopy( // editorInput); // if (unit != null) { // return unit.getDartProject(); // } // return null; // } private IRegion getRegion(IDocument document, ILineRange lineRange) throws BadLocationException { final int startLine = lineRange.getStartLine(); int offset = document.getLineOffset(startLine); final int numberOfLines = lineRange.getNumberOfLines(); if (numberOfLines < 1) { return new Region(offset, 0); } int endLine = startLine + numberOfLines - 1; int endOffset; if (fSharedState.fEditor.isBlockSelectionModeEnabled()) { // in column mode, don't select the last delimiter as we count an // empty selected line IRegion endLineInfo = document.getLineInformation(endLine); endOffset = endLineInfo.getOffset() + endLineInfo.getLength(); } else { endOffset = document.getLineOffset(endLine) + document.getLineLength(endLine); } return new Region(offset, endOffset - offset); } /** * Computes the region of the skipped line given the text block to be moved. If * <code>fUpwards</code> is <code>true</code>, the line above <code>selection</code> is selected, * otherwise the line below. * * @param document the document <code>selection</code> refers to * @param selection the selection on <code>document</code> that will be moved. * @return the region comprising the line that <code>selection</code> will be moved over, without * its terminating delimiter. */ private ITextSelection getSkippedLine(IDocument document, ITextSelection selection) { int skippedLineN = (fUpwards ? selection.getStartLine() - 1 : selection.getEndLine() + 1); if (skippedLineN > document.getNumberOfLines() || (!fCopy && (skippedLineN < 0 || skippedLineN == document.getNumberOfLines()))) { return null; } try { if (fCopy && skippedLineN == -1) { skippedLineN = 0; } IRegion line = document.getLineInformation(skippedLineN); return new TextSelection(document, line.getOffset(), line.getLength()); } catch (BadLocationException e) { // only happens on concurrent modifications return null; } } /** * Checks for white space in a string. * * @param string the string to be checked or <code>null</code> * @return <code>true</code> if <code>string</code> contains only white space or is * <code>null</code>, <code>false</code> otherwise */ private boolean isWhitespace(String string) { return string == null ? true : string.trim().length() == 0; } /** * Performs similar to AbstractTextEditor.selectAndReveal, but does not update the viewers * highlight area. * * @param viewer the viewer that we want to select on * @param offset the offset of the selection * @param length the length of the selection */ private void selectAndReveal(ITextViewer viewer, int offset, int length) { // invert selection to avoid jumping to the end of the selection in // st.showSelection() viewer.setSelectedRange(offset + length, -length); // viewer.revealRange(offset, length); // will trigger jumping StyledText st = viewer.getTextWidget(); if (st != null) { st.showSelection(); // only minimal scrolling } } /** * Displays information in the status line why a line move is not possible */ private void showStatus() { IEditorStatusLine status = (IEditorStatusLine) fSharedState.fEditor.getAdapter(IEditorStatusLine.class); if (status == null) { return; } status.setMessage(false, DartEditorMessages.Editor_MoveLines_IllegalMove_status, null); } }