/*
* 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);
}
}