001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.jdt.internal.ui.javaeditor;
011:
012: import java.util.ResourceBundle;
013:
014: import org.eclipse.core.runtime.Assert;
015:
016: import org.eclipse.swt.custom.StyledText;
017: import org.eclipse.swt.graphics.Point;
018: import org.eclipse.swt.widgets.Event;
019:
020: import org.eclipse.jface.text.BadLocationException;
021: import org.eclipse.jface.text.IDocument;
022: import org.eclipse.jface.text.IRegion;
023: import org.eclipse.jface.text.IRewriteTarget;
024: import org.eclipse.jface.text.ITextSelection;
025: import org.eclipse.jface.text.ITextViewer;
026: import org.eclipse.jface.text.ITextViewerExtension5;
027: import org.eclipse.jface.text.Region;
028: import org.eclipse.jface.text.TextSelection;
029: import org.eclipse.jface.text.TextUtilities;
030: import org.eclipse.jface.text.source.ILineRange;
031: import org.eclipse.jface.text.source.ISourceViewer;
032: import org.eclipse.jface.text.source.LineRange;
033:
034: import org.eclipse.ui.IEditorInput;
035: import org.eclipse.ui.texteditor.IEditorStatusLine;
036: import org.eclipse.ui.texteditor.ITextEditor;
037: import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
038: import org.eclipse.ui.texteditor.TextEditorAction;
039:
040: import org.eclipse.jdt.core.ICompilationUnit;
041: import org.eclipse.jdt.core.IJavaProject;
042:
043: import org.eclipse.jdt.internal.ui.JavaPlugin;
044: import org.eclipse.jdt.internal.ui.javaeditor.IndentUtil.IndentResult;
045:
046: /**
047: * Action for moving selected lines in a Java editor.
048: * @since 3.1
049: */
050: public class JavaMoveLinesAction extends TextEditorAction {
051:
052: /**
053: * State shared by the Move / Copy lines action quadruple.
054: * @since 3.1
055: */
056: private static final class SharedState {
057: /** The compilation unit editor that all four actions operate on. */
058: public CompilationUnitEditor fEditor;
059: /**
060: * The indent token shared by all four actions.
061: */
062: public IndentResult fResult = null;
063: /**
064: * Set to true before modifying the document, to false after.
065: */
066: boolean fIsChanging = false;
067:
068: /** <code>true</code> if a compound move / copy is going on. */
069: private boolean fEditInProgress = false;
070: /** The exit strategy that will detect the ending of a compound edit */
071: private final CompoundEditExitStrategy fExitStrategy;
072:
073: public SharedState(CompilationUnitEditor editor) {
074: fEditor = editor;
075: fExitStrategy = new CompoundEditExitStrategy(new String[] {
076: ITextEditorActionDefinitionIds.MOVE_LINES_UP,
077: ITextEditorActionDefinitionIds.MOVE_LINES_DOWN,
078: ITextEditorActionDefinitionIds.COPY_LINES_UP,
079: ITextEditorActionDefinitionIds.COPY_LINES_DOWN });
080: fExitStrategy
081: .addCompoundListener(new ICompoundEditListener() {
082: public void endCompoundEdit() {
083: SharedState.this .endCompoundEdit();
084: }
085: });
086: }
087:
088: /**
089: * Ends the compound change.
090: */
091: public void beginCompoundEdit() {
092: if (fEditInProgress || fEditor == null)
093: return;
094:
095: fEditInProgress = true;
096:
097: fExitStrategy.arm(fEditor.getViewer());
098:
099: IRewriteTarget target = (IRewriteTarget) fEditor
100: .getAdapter(IRewriteTarget.class);
101: if (target != null) {
102: target.beginCompoundChange();
103: }
104: }
105:
106: /**
107: * Ends the compound change.
108: */
109: public void endCompoundEdit() {
110: if (!fEditInProgress || fEditor == null)
111: return;
112:
113: fExitStrategy.disarm();
114:
115: IRewriteTarget target = (IRewriteTarget) fEditor
116: .getAdapter(IRewriteTarget.class);
117: if (target != null) {
118: target.endCompoundChange();
119: }
120:
121: fResult = null;
122: fEditInProgress = false;
123: }
124: }
125:
126: /* keys */
127:
128: /** Key for status message upon illegal move. <p>Value {@value}</p> */
129:
130: /* state variables - define what this action does */
131:
132: /** <code>true</code> if lines are shifted upwards, <code>false</code> otherwise. */
133: private final boolean fUpwards;
134: /** <code>true</code> if lines are to be copied instead of moved. */
135: private final boolean fCopy;
136: /** The shared state of the move/copy action quadruple. */
137: private final SharedState fSharedState;
138:
139: /**
140: * Creates the quadruple of move and copy actions. The returned array contains
141: * the actions in the following order:
142: * [0] move up
143: * [1] move down
144: * [2] copy up (duplicate)
145: * [3] copy down (duplicate & select)
146: * @param bundle the resource bundle
147: * @param editor the editor
148: * @return the quadruple of actions
149: */
150: public static JavaMoveLinesAction[] createMoveCopyActionSet(
151: ResourceBundle bundle, CompilationUnitEditor editor) {
152: SharedState state = new SharedState(editor);
153: JavaMoveLinesAction[] actions = new JavaMoveLinesAction[4];
154: actions[0] = new JavaMoveLinesAction(bundle,
155: "Editor.MoveLinesUp.", true, false, state); //$NON-NLS-1$
156: actions[1] = new JavaMoveLinesAction(bundle,
157: "Editor.MoveLinesDown.", false, false, state); //$NON-NLS-1$
158: actions[2] = new JavaMoveLinesAction(bundle,
159: "Editor.CopyLineUp.", true, true, state); //$NON-NLS-1$
160: actions[3] = new JavaMoveLinesAction(bundle,
161: "Editor.CopyLineDown.", false, true, state); //$NON-NLS-1$
162: return actions;
163: }
164:
165: /*
166: * @see org.eclipse.ui.texteditor.TextEditorAction#setEditor(org.eclipse.ui.texteditor.ITextEditor)
167: */
168: public void setEditor(ITextEditor editor) {
169: Assert.isTrue(editor instanceof CompilationUnitEditor);
170: super .setEditor(editor);
171: if (fSharedState != null)
172: fSharedState.fEditor = (CompilationUnitEditor) editor;
173: }
174:
175: /**
176: * Creates and initializes the action for the given text editor.
177: * The action configures its visual representation from the given resource
178: * bundle.
179: *
180: * @param bundle the resource bundle
181: * @param prefix a prefix to be prepended to the various resource keys
182: * (described in <code>ResourceAction</code> constructor), or <code>null</code> if none
183: * @param upwards <code>true</code>if the selected lines should be moved upwards,
184: * <code>false</code> if downwards
185: * @param copy if <code>true</code>, the action will copy lines instead of moving them
186: * @param state the shared state
187: * @see TextEditorAction#TextEditorAction(ResourceBundle, String, ITextEditor)
188: */
189: private JavaMoveLinesAction(ResourceBundle bundle, String prefix,
190: boolean upwards, boolean copy, SharedState state) {
191: super (bundle, prefix, state.fEditor);
192: fUpwards = upwards;
193: fCopy = copy;
194: fSharedState = state;
195: update();
196: }
197:
198: /**
199: * Checks if <code>selection</code> is contained by the visible region of <code>viewer</code>.
200: * As a special case, a selection is considered contained even if it extends over the visible
201: * region, but the extension stays on a partially contained line and contains only white space.
202: *
203: * @param selection the selection to be checked
204: * @param viewer the viewer displaying a visible region of <code>selection</code>'s document.
205: * @return <code>true</code>, if <code>selection</code> is contained, <code>false</code> otherwise.
206: */
207: private boolean containedByVisibleRegion(ITextSelection selection,
208: ISourceViewer viewer) {
209: int min = selection.getOffset();
210: int max = min + selection.getLength();
211: IDocument document = viewer.getDocument();
212:
213: IRegion visible;
214: if (viewer instanceof ITextViewerExtension5)
215: visible = ((ITextViewerExtension5) viewer)
216: .getModelCoverage();
217: else
218: visible = viewer.getVisibleRegion();
219:
220: int visOffset = visible.getOffset();
221: try {
222: if (visOffset > min) {
223: if (document.getLineOfOffset(visOffset) != selection
224: .getStartLine())
225: return false;
226: if (!isWhitespace(document.get(min, visOffset - min))) {
227: showStatus();
228: return false;
229: }
230: }
231: int visEnd = visOffset + visible.getLength();
232: if (visEnd < max) {
233: if (document.getLineOfOffset(visEnd) != selection
234: .getEndLine())
235: return false;
236: if (!isWhitespace(document.get(visEnd, max - visEnd))) {
237: showStatus();
238: return false;
239: }
240: }
241: return true;
242: } catch (BadLocationException e) {
243: }
244: return false;
245: }
246:
247: /**
248: * Given a selection on a document, computes the lines fully or partially covered by
249: * <code>selection</code>. A line in the document is considered covered if
250: * <code>selection</code> comprises any characters on it, including the terminating delimiter.
251: * <p>Note that the last line in a selection is not considered covered if the selection only
252: * comprises the line delimiter at its beginning (that is considered part of the second last
253: * line).
254: * As a special case, if the selection is empty, a line is considered covered if the caret is
255: * at any position in the line, including between the delimiter and the start of the line. The
256: * line containing the delimiter is not considered covered in that case.
257: * </p>
258: *
259: * @param document the document <code>selection</code> refers to
260: * @param selection a selection on <code>document</code>
261: * @param viewer the <code>ISourceViewer</code> displaying <code>document</code>
262: * @return a selection describing the range of lines (partially) covered by
263: * <code>selection</code>, without any terminating line delimiters
264: * @throws BadLocationException if the selection is out of bounds (when the underlying document has changed during the call)
265: */
266: private ITextSelection getMovingSelection(IDocument document,
267: ITextSelection selection, ISourceViewer viewer)
268: throws BadLocationException {
269: int low = document.getLineOffset(selection.getStartLine());
270: int endLine = selection.getEndLine();
271: int high = document.getLineOffset(endLine)
272: + document.getLineLength(endLine);
273:
274: // get everything up to last line without its delimiter
275: String delim = document.getLineDelimiter(endLine);
276: if (delim != null)
277: high -= delim.length();
278:
279: return new TextSelection(document, low, high - low);
280: }
281:
282: /**
283: * Computes the region of the skipped line given the text block to be moved. If
284: * <code>fUpwards</code> is <code>true</code>, the line above <code>selection</code>
285: * is selected, otherwise the line below.
286: *
287: * @param document the document <code>selection</code> refers to
288: * @param selection the selection on <code>document</code> that will be moved.
289: * @return the region comprising the line that <code>selection</code> will be moved over, without its terminating delimiter.
290: */
291: private ITextSelection getSkippedLine(IDocument document,
292: ITextSelection selection) {
293: int skippedLineN = (fUpwards ? selection.getStartLine() - 1
294: : selection.getEndLine() + 1);
295: if (skippedLineN > document.getNumberOfLines()
296: || (!fCopy && (skippedLineN < 0 || skippedLineN == document
297: .getNumberOfLines())))
298: return null;
299: try {
300: if (fCopy && skippedLineN == -1)
301: skippedLineN = 0;
302: IRegion line = document.getLineInformation(skippedLineN);
303: return new TextSelection(document, line.getOffset(), line
304: .getLength());
305: } catch (BadLocationException e) {
306: // only happens on concurrent modifications
307: return null;
308: }
309: }
310:
311: /**
312: * Checks for white space in a string.
313: *
314: * @param string the string to be checked or <code>null</code>
315: * @return <code>true</code> if <code>string</code> contains only white space or is
316: * <code>null</code>, <code>false</code> otherwise
317: */
318: private boolean isWhitespace(String string) {
319: return string == null ? true : string.trim().length() == 0;
320: }
321:
322: /*
323: * @see org.eclipse.jface.action.IAction#run()
324: */
325: public void runWithEvent(Event event) {
326:
327: // get involved objects
328: if (fSharedState.fEditor == null)
329: return;
330:
331: if (!validateEditorInputState())
332: return;
333:
334: ISourceViewer viewer = fSharedState.fEditor.getViewer();
335: if (viewer == null)
336: return;
337:
338: IDocument document = viewer.getDocument();
339: if (document == null)
340: return;
341:
342: StyledText widget = viewer.getTextWidget();
343: if (widget == null)
344: return;
345:
346: // get selection
347: Point p = viewer.getSelectedRange();
348: if (p == null)
349: return;
350:
351: ITextSelection sel = new TextSelection(document, p.x, p.y);
352:
353: ITextSelection skippedLine = getSkippedLine(document, sel);
354: if (skippedLine == null)
355: return;
356:
357: try {
358:
359: ITextSelection movingArea = getMovingSelection(document,
360: sel, viewer);
361:
362: // if either the skipped line or the moving lines are outside the widget's
363: // visible area, bail out
364: if (!containedByVisibleRegion(movingArea, viewer)
365: || !containedByVisibleRegion(skippedLine, viewer))
366: return;
367:
368: // get the content to be moved around: the moving (selected) area and the skipped line
369: String moving = movingArea.getText();
370: String skipped = skippedLine.getText();
371: if (moving == null || skipped == null
372: || document.getLength() == 0)
373: return;
374:
375: String delim;
376: String insertion;
377: int offset;
378: if (fUpwards) {
379: delim = document.getLineDelimiter(skippedLine
380: .getEndLine());
381: if (fCopy) {
382: delim = TextUtilities
383: .getDefaultLineDelimiter(document);
384: insertion = moving + delim;
385: offset = movingArea.getOffset();
386: } else {
387: Assert.isNotNull(delim);
388: insertion = moving + delim + skipped;
389: offset = skippedLine.getOffset();
390: }
391: } else {
392: delim = document.getLineDelimiter(movingArea
393: .getEndLine());
394: if (fCopy) {
395: if (delim == null) {
396: delim = TextUtilities
397: .getDefaultLineDelimiter(document);
398: insertion = delim + moving;
399: } else
400: insertion = moving + delim;
401: offset = skippedLine.getOffset();
402: } else {
403: Assert.isNotNull(delim);
404: insertion = skipped + delim + moving;
405: offset = movingArea.getOffset();
406: }
407: }
408: int lenght = fCopy ? 0 : insertion.length();
409:
410: // modify the document
411: ILineRange selectionBefore = getLineRange(document,
412: movingArea);
413:
414: if (fCopy)
415: fSharedState.endCompoundEdit();
416: fSharedState.beginCompoundEdit();
417: fSharedState.fIsChanging = true;
418:
419: document.replace(offset, lenght, insertion);
420:
421: ILineRange selectionAfter;
422: if (fUpwards && fCopy)
423: selectionAfter = selectionBefore;
424: else if (fUpwards)
425: selectionAfter = new LineRange(selectionBefore
426: .getStartLine() - 1, selectionBefore
427: .getNumberOfLines());
428: else if (fCopy)
429: selectionAfter = new LineRange(selectionBefore
430: .getStartLine()
431: + selectionBefore.getNumberOfLines(),
432: selectionBefore.getNumberOfLines());
433: else
434: selectionAfter = new LineRange(selectionBefore
435: .getStartLine() + 1, selectionBefore
436: .getNumberOfLines());
437:
438: fSharedState.fResult = IndentUtil.indentLines(document,
439: selectionAfter, getProject(), fSharedState.fResult);
440:
441: // move the selection along
442: IRegion region = getRegion(document, selectionAfter);
443: selectAndReveal(viewer, region.getOffset(), region
444: .getLength());
445:
446: } catch (BadLocationException x) {
447: // won't happen without concurrent modification - bail out
448: return;
449: } finally {
450: fSharedState.fIsChanging = false;
451: if (fCopy)
452: fSharedState.endCompoundEdit();
453: }
454: }
455:
456: private IJavaProject getProject() {
457: IEditorInput editorInput = fSharedState.fEditor
458: .getEditorInput();
459: ICompilationUnit unit = JavaPlugin.getDefault()
460: .getWorkingCopyManager().getWorkingCopy(editorInput);
461: if (unit != null)
462: return unit.getJavaProject();
463: return null;
464: }
465:
466: private ILineRange getLineRange(IDocument document,
467: ITextSelection selection) throws BadLocationException {
468: final int offset = selection.getOffset();
469: int startLine = document.getLineOfOffset(offset);
470: int endOffset = offset + selection.getLength();
471: int endLine = document.getLineOfOffset(endOffset);
472: final int nLines = endLine - startLine + 1;
473: return new LineRange(startLine, nLines);
474: }
475:
476: private IRegion getRegion(IDocument document, ILineRange lineRange)
477: throws BadLocationException {
478: final int startLine = lineRange.getStartLine();
479: int offset = document.getLineOffset(startLine);
480: final int numberOfLines = lineRange.getNumberOfLines();
481: if (numberOfLines < 1)
482: return new Region(offset, 0);
483: int endLine = startLine + numberOfLines - 1;
484: int endOffset = document.getLineOffset(endLine)
485: + document.getLineLength(endLine);
486: return new Region(offset, endOffset - offset);
487: }
488:
489: /**
490: * Performs similar to AbstractTextEditor.selectAndReveal, but does not update
491: * the viewers highlight area.
492: *
493: * @param viewer the viewer that we want to select on
494: * @param offset the offset of the selection
495: * @param length the length of the selection
496: */
497: private void selectAndReveal(ITextViewer viewer, int offset,
498: int length) {
499: // invert selection to avoid jumping to the end of the selection in st.showSelection()
500: viewer.setSelectedRange(offset + length, -length);
501: //viewer.revealRange(offset, length); // will trigger jumping
502: StyledText st = viewer.getTextWidget();
503: if (st != null)
504: st.showSelection(); // only minimal scrolling
505: }
506:
507: /**
508: * Displays information in the status line why a line move is not possible
509: */
510: private void showStatus() {
511: IEditorStatusLine status = (IEditorStatusLine) fSharedState.fEditor
512: .getAdapter(IEditorStatusLine.class);
513: if (status == null)
514: return;
515: status.setMessage(false,
516: JavaEditorMessages.Editor_MoveLines_IllegalMove_status,
517: null);
518: }
519:
520: /*
521: * @see org.eclipse.ui.texteditor.IUpdate#update()
522: */
523: public void update() {
524: super.update();
525:
526: if (isEnabled())
527: setEnabled(canModifyEditor());
528:
529: }
530: }
|