/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.editor.actions; import java.util.ResourceBundle; 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.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.AbstractTextEditor; import org.eclipse.ui.texteditor.IEditorStatusLine; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.TextEditorAction; import org.python.pydev.core.docutils.ParsingUtils; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.docutils.StringUtils; import org.python.pydev.core.log.Log; import org.python.pydev.editor.PyEdit; import org.python.pydev.editor.autoedit.PyAutoIndentStrategy; import com.aptana.shared_core.utils.DocCmd; /** * Base class for actions that do a move action (Alt+Up or Alt+Down). * * Subclasses just need to decide whether to go up or down. * * @author Fabio */ public abstract class PyMoveLineAction extends TextEditorAction { protected PyEdit pyEdit; protected PyMoveLineAction(ResourceBundle bundle, String prefix, PyEdit editor) { super(bundle, prefix, editor); this.pyEdit = editor; update(); } public void runWithEvent(Event event) { run(); } public void run() { // get involved objects if (pyEdit == null) { return; } if (!validateEditorInputState()) return; ISourceViewer viewer = pyEdit.getEditorSourceViewer(); 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(); move(pyEdit, viewer, document, sel); } public void move(PyEdit pyEdit, ISourceViewer viewer, IDocument document, ITextSelection sel) { if (sel.isEmpty()) return; ITextSelection skippedLine = getSkippedLine(document, sel); if (skippedLine == null) return; ITextSelection movingArea; try { try { movingArea = getMovingSelection(document, sel); } catch (BadLocationException e) { return; //selection is out of range } // 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; PySelection skippedPs = new PySelection(document, skippedLine); // 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; String delim; String insertion; int offset; int length; ILineRange selectionBefore = getLineRange(document, movingArea); IRewriteTarget target = null; if (pyEdit != null) { target = (IRewriteTarget) pyEdit.getAdapter(IRewriteTarget.class); if (target != null) { target.beginCompoundChange(); if (!getMoveUp()) { //When going up we'll just do a single document change, so, there's //no need to set the redraw. target.setRedraw(false); } } } ILineRange selectionAfter; boolean isStringPartition; try { if (getMoveUp()) { //check partition in the start of the skipped line isStringPartition = ParsingUtils.isStringPartition(document, skippedLine.getOffset()); delim = document.getLineDelimiter(skippedLine.getEndLine()); Assert.isNotNull(delim); offset = skippedLine.getOffset(); length = moving.length() + delim.length() + skipped.length(); } else { //check partition in the start of the line after the skipped line int offsetToCheckPartition; if (skippedLine.getEndLine() == document.getNumberOfLines() - 1) { offsetToCheckPartition = document.getLength() - 1; //check the last document char } else { offsetToCheckPartition = skippedLine.getOffset() + skippedLine.getLength(); //that's always the '\n' of the line } isStringPartition = ParsingUtils.isStringPartition(document, offsetToCheckPartition); delim = document.getLineDelimiter(movingArea.getEndLine()); Assert.isNotNull(delim); offset = movingArea.getOffset(); //When going down, we need to remove the movingArea to compute the new indentation //properly (otherwise we'd use that text being moved on the compute algorithm) document.replace(movingArea.getOffset(), movingArea.getLength() + delim.length(), ""); length = skipped.length(); int pos = skippedPs.getAbsoluteCursorOffset() - (movingArea.getLength() + delim.length()); skippedPs.setSelection(pos, pos); } PyAutoIndentStrategy indentStrategy = null; if (pyEdit != null) { indentStrategy = pyEdit.getAutoEditStrategy(); } if (indentStrategy == null) { indentStrategy = new PyAutoIndentStrategy(); } if (!isStringPartition) { if (indentStrategy.getIndentPrefs().getSmartLineMove()) { String prevExpectedIndent = calculateNewIndentationString(document, skippedPs, indentStrategy); if (prevExpectedIndent != null) { moving = StringUtils.removeWhitespaceColumnsToLeftAndApplyIndent(moving, prevExpectedIndent, false); } } } if (getMoveUp()) { insertion = moving + delim + skipped; } else { insertion = skipped + delim + moving; } // modify the document document.replace(offset, length, insertion); if (getMoveUp()) { selectionAfter = new LineRange(selectionBefore.getStartLine() - 1, selectionBefore.getNumberOfLines()); } else { selectionAfter = new LineRange(selectionBefore.getStartLine() + 1, selectionBefore.getNumberOfLines()); } } finally { if (target != null) { target.endCompoundChange(); if (!getMoveUp()) { target.setRedraw(true); } } } // move the selection along IRegion region = getRegion(document, selectionAfter); selectAndReveal(viewer, region.getOffset(), region.getLength()); } catch (BadLocationException e) { Log.log(e); return; } } /** * This method will return the indentation that should be applied for the moving text. */ private String calculateNewIndentationString(IDocument document, PySelection skippedPs, PyAutoIndentStrategy indentStrategy) throws BadLocationException { int cursorLine = skippedPs.getCursorLine(); int line = cursorLine; if (getMoveUp()) { if (cursorLine == 0) { String cursorLineContents = skippedPs.getCursorLineContents(); int firstCharPosition = PySelection.getFirstCharPosition(cursorLineContents); return cursorLineContents.substring(0, firstCharPosition); } line = cursorLine - 1; if (line < 0) { return null; } } //Go to a non-empty line! String line2 = skippedPs.getLine(line); while (line > 0 && (line2.startsWith("#") || line2.trim().length() == 0)) { line--; line2 = skippedPs.getLine(line); } DocumentCommand command = new DocCmd(skippedPs.getEndLineOffset(line), 0, "\n"); indentStrategy.customizeDocumentCommand(document, command); return command.text.substring(1); } 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); } /** * 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) { if (viewer == null) { return; // in tests } // 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 } 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; boolean blockSelectionModeEnabled = false; try { blockSelectionModeEnabled = ((AbstractTextEditor) getTextEditor()).isBlockSelectionModeEnabled(); } catch (Throwable e) { //Ignore (not available before 3.5) } if (blockSelectionModeEnabled) { // in block selection 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); } protected abstract boolean getMoveUp(); /* * @see org.eclipse.ui.texteditor.IUpdate#update() */ public void update() { super.update(); if (isEnabled()) { setEnabled(canModifyEditor()); } } /** * 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 = (getMoveUp() ? selection.getStartLine() - 1 : selection.getEndLine() + 1); if (skippedLineN > document.getNumberOfLines() || ((skippedLineN < 0 || skippedLineN == document.getNumberOfLines()))) return null; try { IRegion line = document.getLineInformation(skippedLineN); return new TextSelection(document, line.getOffset(), line.getLength()); } catch (BadLocationException e) { // only happens on concurrent modifications return null; } } /** * 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> * @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) 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); } /** * 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) { if (viewer == null) { return true; //in tests } 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; } /** * 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; } /** * Displays information in the status line why a line move is not possible */ private void showStatus() { ITextEditor textEditor = getTextEditor(); IEditorStatusLine status = (IEditorStatusLine) textEditor.getAdapter(IEditorStatusLine.class); if (status == null) return; status.setMessage(false, "Move not possible - Uncheck \"Show Source of Selected Element Only\" to see the entire document", null); } }