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.ui.texteditor;
011:
012: import java.util.ResourceBundle;
013:
014: import org.eclipse.swt.custom.StyledText;
015: import org.eclipse.swt.graphics.Point;
016: import org.eclipse.swt.widgets.Event;
017:
018: import org.eclipse.core.runtime.Assert;
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.TextSelection;
028: import org.eclipse.jface.text.TextUtilities;
029: import org.eclipse.jface.text.source.ISourceViewer;
030:
031: import org.eclipse.ui.internal.texteditor.CompoundEditExitStrategy;
032: import org.eclipse.ui.internal.texteditor.ICompoundEditListener;
033:
034: /**
035: * Action for moving selected lines in an editor.
036: * @since 3.0
037: */
038: public class MoveLinesAction extends TextEditorAction {
039:
040: /* configuration variables - define what this action does */
041:
042: /** <code>true</code> if lines are shifted upwards, <code>false</code> otherwise. */
043: private final boolean fUpwards;
044: /** <code>true</code> if lines are to be copied instead of moved. */
045: private final boolean fCopy;
046: /** The editor we are working on. */
047: private final AbstractTextEditor fEditor;
048:
049: /* compound members of this action */
050:
051: /**
052: * The exit strategy that will detect the ending of a compound edit.
053: * @since 3.1
054: */
055: private final CompoundEditExitStrategy fStrategy;
056:
057: /* process variables - may change in every run() */
058:
059: /**
060: * Set to <code>true</code> by <code>getMovingSelection</code> if the resulting selection
061: * should include the last delimiter.
062: */
063: private boolean fAddDelimiter;
064: /** <code>true</code> if a compound move / copy is going on. */
065: private boolean fEditInProgress = false;
066:
067: /**
068: * Creates and initializes the action for the given text editor.
069: * The action configures its visual representation from the given resource
070: * bundle.
071: *
072: * @param bundle the resource bundle
073: * @param prefix a prefix to be prepended to the various resource keys
074: * (described in <code>ResourceAction</code> constructor), or <code>null</code> if none
075: * @param editor the text editor
076: * @param upwards <code>true</code>if the selected lines should be moved upwards,
077: * <code>false</code> if downwards
078: * @param copy if <code>true</code>, the action will copy lines instead of moving them
079: * @see TextEditorAction#TextEditorAction(ResourceBundle, String, ITextEditor)
080: */
081: public MoveLinesAction(ResourceBundle bundle, String prefix,
082: AbstractTextEditor editor, boolean upwards, boolean copy) {
083: super (bundle, prefix, editor);
084: fEditor = editor;
085: fUpwards = upwards;
086: fCopy = copy;
087: String[] commandIds = copy ? new String[] {
088: ITextEditorActionDefinitionIds.COPY_LINES_UP,
089: ITextEditorActionDefinitionIds.COPY_LINES_DOWN }
090: : new String[] {
091: ITextEditorActionDefinitionIds.MOVE_LINES_UP,
092: ITextEditorActionDefinitionIds.MOVE_LINES_DOWN };
093: fStrategy = new CompoundEditExitStrategy(commandIds);
094: fStrategy.addCompoundListener(new ICompoundEditListener() {
095: public void endCompoundEdit() {
096: MoveLinesAction.this .endCompoundEdit();
097: }
098: });
099: update();
100: }
101:
102: /**
103: * Ends the compound change.
104: */
105: private void beginCompoundEdit() {
106: if (fEditInProgress || fEditor == null)
107: return;
108:
109: fEditInProgress = true;
110:
111: fStrategy.arm(fEditor.getSourceViewer());
112:
113: IRewriteTarget target = (IRewriteTarget) fEditor
114: .getAdapter(IRewriteTarget.class);
115: if (target != null) {
116: target.beginCompoundChange();
117: }
118: }
119:
120: /**
121: * Checks if <code>selection</code> is contained by the visible region of <code>viewer</code>.
122: * As a special case, a selection is considered contained even if it extends over the visible
123: * region, but the extension stays on a partially contained line and contains only white space.
124: *
125: * @param selection the selection to be checked
126: * @param viewer the viewer displaying a visible region of <code>selection</code>'s document.
127: * @return <code>true</code>, if <code>selection</code> is contained, <code>false</code> otherwise.
128: */
129: private boolean containedByVisibleRegion(ITextSelection selection,
130: ISourceViewer viewer) {
131: int min = selection.getOffset();
132: int max = min + selection.getLength();
133: IDocument document = viewer.getDocument();
134:
135: IRegion visible;
136: if (viewer instanceof ITextViewerExtension5)
137: visible = ((ITextViewerExtension5) viewer)
138: .getModelCoverage();
139: else
140: visible = viewer.getVisibleRegion();
141:
142: int visOffset = visible.getOffset();
143: try {
144: if (visOffset > min) {
145: if (document.getLineOfOffset(visOffset) != selection
146: .getStartLine())
147: return false;
148: if (!isWhitespace(document.get(min, visOffset - min))) {
149: showStatus();
150: return false;
151: }
152: }
153: int visEnd = visOffset + visible.getLength();
154: if (visEnd < max) {
155: if (document.getLineOfOffset(visEnd) != selection
156: .getEndLine())
157: return false;
158: if (!isWhitespace(document.get(visEnd, max - visEnd))) {
159: showStatus();
160: return false;
161: }
162: }
163: return true;
164: } catch (BadLocationException e) {
165: }
166: return false;
167: }
168:
169: /**
170: * Ends the compound change.
171: */
172: private void endCompoundEdit() {
173: if (!fEditInProgress || fEditor == null)
174: return;
175:
176: IRewriteTarget target = (IRewriteTarget) fEditor
177: .getAdapter(IRewriteTarget.class);
178: if (target != null) {
179: target.endCompoundChange();
180: }
181:
182: fEditInProgress = false;
183: }
184:
185: /**
186: * Given a selection on a document, computes the lines fully or partially covered by
187: * <code>selection</code>. A line in the document is considered covered if
188: * <code>selection</code> comprises any characters on it, including the terminating delimiter.
189: * <p>Note that the last line in a selection is not considered covered if the selection only
190: * comprises the line delimiter at its beginning (that is considered part of the second last
191: * line).
192: * As a special case, if the selection is empty, a line is considered covered if the caret is
193: * at any position in the line, including between the delimiter and the start of the line. The
194: * line containing the delimiter is not considered covered in that case.
195: * </p>
196: *
197: * @param document the document <code>selection</code> refers to
198: * @param selection a selection on <code>document</code>
199: * @param viewer the <code>ISourceViewer</code> displaying <code>document</code>
200: * @return a selection describing the range of lines (partially) covered by
201: * <code>selection</code>, without any terminating line delimiters
202: * @throws BadLocationException if the selection is out of bounds (when the underlying document has changed during the call)
203: */
204: private ITextSelection getMovingSelection(IDocument document,
205: ITextSelection selection, ISourceViewer viewer)
206: throws BadLocationException {
207: int low = document.getLineOffset(selection.getStartLine());
208: int endLine = selection.getEndLine();
209: int high = document.getLineOffset(endLine)
210: + document.getLineLength(endLine);
211:
212: // get everything up to last line without its delimiter
213: String delim = document.getLineDelimiter(endLine);
214: if (delim != null)
215: high -= delim.length();
216:
217: // the new selection will cover the entire lines being moved, except for the last line's
218: // delimiter. The exception to this rule is an empty last line, which will stay covered
219: // including its delimiter
220: if (delim != null
221: && document.getLineLength(endLine) == delim.length())
222: fAddDelimiter = true;
223: else
224: fAddDelimiter = false;
225:
226: return new TextSelection(document, low, high - low);
227: }
228:
229: /**
230: * Computes the region of the skipped line given the text block to be moved. If
231: * <code>fUpwards</code> is <code>true</code>, the line above <code>selection</code>
232: * is selected, otherwise the line below.
233: *
234: * @param document the document <code>selection</code> refers to
235: * @param selection the selection on <code>document</code> that will be moved.
236: * @return the region comprising the line that <code>selection</code> will be moved over, without its terminating delimiter.
237: */
238: private ITextSelection getSkippedLine(IDocument document,
239: ITextSelection selection) {
240: int skippedLineN = (fUpwards ? selection.getStartLine() - 1
241: : selection.getEndLine() + 1);
242: if (skippedLineN > document.getNumberOfLines()
243: || (!fCopy && (skippedLineN < 0 || skippedLineN == document
244: .getNumberOfLines())))
245: return null;
246: try {
247: if (fCopy && skippedLineN == -1)
248: skippedLineN = 0;
249: IRegion line = document.getLineInformation(skippedLineN);
250: return new TextSelection(document, line.getOffset(), line
251: .getLength());
252: } catch (BadLocationException e) {
253: // only happens on concurrent modifications
254: return null;
255: }
256: }
257:
258: /**
259: * Checks for white space in a string.
260: *
261: * @param string the string to be checked or <code>null</code>
262: * @return <code>true</code> if <code>string</code> contains only white space or is
263: * <code>null</code>, <code>false</code> otherwise
264: */
265: private boolean isWhitespace(String string) {
266: return string == null ? true : string.trim().length() == 0;
267: }
268:
269: /*
270: * @see org.eclipse.jface.action.IAction#run()
271: */
272: public void runWithEvent(Event event) {
273:
274: // get involved objects
275: if (fEditor == null)
276: return;
277:
278: if (!validateEditorInputState())
279: return;
280:
281: ISourceViewer viewer = fEditor.getSourceViewer();
282: if (viewer == null)
283: return;
284:
285: IDocument document = viewer.getDocument();
286: if (document == null)
287: return;
288:
289: StyledText widget = viewer.getTextWidget();
290: if (widget == null)
291: return;
292:
293: // get selection
294: Point p = viewer.getSelectedRange();
295: if (p == null)
296: return;
297:
298: ITextSelection sel = new TextSelection(document, p.x, p.y);
299:
300: ITextSelection skippedLine = getSkippedLine(document, sel);
301: if (skippedLine == null)
302: return;
303:
304: try {
305:
306: ITextSelection movingArea = getMovingSelection(document,
307: sel, viewer);
308:
309: // if either the skipped line or the moving lines are outside the widget's
310: // visible area, bail out
311: if (!containedByVisibleRegion(movingArea, viewer)
312: || !containedByVisibleRegion(skippedLine, viewer))
313: return;
314:
315: // get the content to be moved around: the moving (selected) area and the skipped line
316: String moving = movingArea.getText();
317: String skipped = skippedLine.getText();
318: if (moving == null || skipped == null
319: || document.getLength() == 0)
320: return;
321:
322: String delim;
323: String insertion;
324: int offset, deviation;
325: if (fUpwards) {
326: delim = document.getLineDelimiter(skippedLine
327: .getEndLine());
328: if (fCopy) {
329: delim = TextUtilities
330: .getDefaultLineDelimiter(document);
331: insertion = moving + delim;
332: offset = movingArea.getOffset();
333: deviation = 0;
334: } else {
335: Assert.isNotNull(delim);
336: insertion = moving + delim + skipped;
337: offset = skippedLine.getOffset();
338: deviation = -skippedLine.getLength()
339: - delim.length();
340: }
341: } else {
342: delim = document.getLineDelimiter(movingArea
343: .getEndLine());
344: if (fCopy) {
345: if (delim == null) {
346: delim = TextUtilities
347: .getDefaultLineDelimiter(document);
348: insertion = delim + moving;
349: } else {
350: insertion = moving + delim;
351: }
352: offset = skippedLine.getOffset();
353: deviation = movingArea.getLength() + delim.length();
354: } else {
355: Assert.isNotNull(delim);
356: insertion = skipped + delim + moving;
357: offset = movingArea.getOffset();
358: deviation = skipped.length() + delim.length();
359: }
360: }
361:
362: // modify the document
363: beginCompoundEdit();
364: if (fCopy) {
365: // fDescription= new EditDescription(offset, 0, insertion.length());
366: document.replace(offset, 0, insertion);
367: } else {
368: // fDescription= new EditDescription(offset, insertion.length(), insertion.length());
369: document.replace(offset, insertion.length(), insertion);
370: }
371:
372: // move the selection along
373: int selOffset = movingArea.getOffset() + deviation;
374: int selLength = movingArea.getLength()
375: + (fAddDelimiter ? delim.length() : 0);
376: if (!(viewer instanceof ITextViewerExtension5))
377: selLength = Math.min(selLength, viewer
378: .getVisibleRegion().getOffset()
379: + viewer.getVisibleRegion().getLength()
380: - selOffset);
381: else {
382: // TODO need to check what is necessary in the projection case
383: }
384: selectAndReveal(viewer, selOffset, selLength);
385: } catch (BadLocationException x) {
386: // won't happen without concurrent modification - bail out
387: return;
388: }
389: }
390:
391: /**
392: * Performs similar to AbstractTextEditor.selectAndReveal, but does not update
393: * the viewers highlight area.
394: *
395: * @param viewer the viewer that we want to select on
396: * @param offset the offset of the selection
397: * @param length the length of the selection
398: */
399: private void selectAndReveal(ITextViewer viewer, int offset,
400: int length) {
401: // invert selection to avoid jumping to the end of the selection in st.showSelection()
402: viewer.setSelectedRange(offset + length, -length);
403: //viewer.revealRange(offset, length); // will trigger jumping
404: StyledText st = viewer.getTextWidget();
405: if (st != null)
406: st.showSelection(); // only minimal scrolling
407: }
408:
409: /**
410: * Displays information in the status line why a line move is not possible
411: */
412: private void showStatus() {
413: IEditorStatusLine status = (IEditorStatusLine) fEditor
414: .getAdapter(IEditorStatusLine.class);
415: if (status == null)
416: return;
417: status.setMessage(false,
418: EditorMessages.Editor_MoveLines_IllegalMove_status,
419: null);
420: }
421:
422: /*
423: * @see org.eclipse.ui.texteditor.IUpdate#update()
424: */
425: public void update() {
426: super.update();
427:
428: if (isEnabled())
429: setEnabled(canModifyEditor());
430:
431: }
432: }
|