/*******************************************************************************
* Copyright (c) 2000, 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package net.sourceforge.phpdt.internal.ui.actions;
import java.util.ResourceBundle;
import net.sourceforge.phpdt.core.JavaCore;
import net.sourceforge.phpdt.core.formatter.DefaultCodeFormatterConstants;
import net.sourceforge.phpdt.internal.ui.text.IPHPPartitions;
import net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner;
import net.sourceforge.phpdt.internal.ui.text.JavaIndenter;
import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager;
import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
import net.sourceforge.phpdt.internal.ui.text.phpdoc.JavaDocAutoIndentStrategy;
import net.sourceforge.phpdt.ui.PreferenceConstants;
import net.sourceforge.phpeclipse.PHPeclipsePlugin;
import net.sourceforge.phpeclipse.phpeditor.PHPEditor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
//incastrix
//import org.eclipse.jface.text.Assert;
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.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorExtension3;
import org.eclipse.ui.texteditor.TextEditorAction;
/**
* Indents a line or range of lines in a Java document to its correct position.
* No complete AST must be present, the indentation is computed using
* heuristics. The algorith used is fast for single lines, but does not store
* any information and therefore not so efficient for large line ranges.
*
* @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner
* @see net.sourceforge.phpdt.internal.ui.text.JavaIndenter
* @since 3.0
*/
public class IndentAction extends TextEditorAction {
/** The caret offset after an indent operation. */
private int fCaretOffset;
/**
* Whether this is the action invoked by TAB. When true
,
* indentation behaves differently to accomodate normal TAB operation.
*/
private final boolean fIsTabAction;
/**
* Creates a new instance.
*
* @param bundle
* the resource bundle
* @param prefix
* the prefix to use for keys in bundle
* @param editor
* the text editor
* @param isTabAction
* whether the action should insert tabs if over the indentation
*/
public IndentAction (ResourceBundle bundle,
String prefix,
ITextEditor editor,
boolean isTabAction) {
super (bundle, prefix, editor);
fIsTabAction = isTabAction;
}
/*
* @see org.eclipse.jface.action.Action#run()
*/
public void run() {
// update has been called by the framework
if (!isEnabled() || !validateEditorInputState())
return;
ITextSelection selection = getSelection();
final IDocument document = getDocument();
if (document != null) {
final int offset = selection.getOffset();
final int length = selection.getLength();
final Position end = new Position(offset + length);
final int firstLine, nLines;
fCaretOffset = -1;
try {
document.addPosition(end);
firstLine = document.getLineOfOffset(offset);
// check for marginal (zero-length) lines
int minusOne = length == 0 ? 0 : 1;
nLines = document.getLineOfOffset(offset + length - minusOne)
- firstLine + 1;
} catch (BadLocationException e) {
// will only happen on concurrent modification
PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin
.getPluginId(), IStatus.OK, "", e)); //$NON-NLS-1$
return;
}
Runnable runnable = new Runnable() {
public void run() {
IRewriteTarget target = (IRewriteTarget) getTextEditor()
.getAdapter(IRewriteTarget.class);
if (target != null) {
target.beginCompoundChange();
target.setRedraw(false);
}
try {
JavaHeuristicScanner scanner = new JavaHeuristicScanner(
document);
JavaIndenter indenter = new JavaIndenter(document,
scanner);
boolean hasChanged = false;
for (int i = 0; i < nLines; i++) {
hasChanged |= indentLine(document, firstLine + i,
offset, indenter, scanner);
}
// update caret position: move to new position when
// indenting just one line
// keep selection when indenting multiple
int newOffset, newLength;
if (fIsTabAction) {
newOffset = fCaretOffset;
newLength = 0;
} else if (nLines > 1) {
newOffset = offset;
newLength = end.getOffset() - offset;
} else {
newOffset = fCaretOffset;
newLength = 0;
}
// always reset the selection if anything was replaced
// but not when we had a singleline nontab invocation
if (newOffset != -1
&& (hasChanged || newOffset != offset || newLength != length))
selectAndReveal(newOffset, newLength);
document.removePosition(end);
} catch (BadLocationException e) {
// will only happen on concurrent modification
PHPeclipsePlugin.log(new Status(IStatus.ERROR,
PHPeclipsePlugin.getPluginId(), IStatus.OK,
"ConcurrentModification in IndentAction", e)); //$NON-NLS-1$
} finally {
if (target != null) {
target.endCompoundChange();
target.setRedraw(true);
}
}
}
};
if (nLines > 50) {
Display display = getTextEditor().getEditorSite()
.getWorkbenchWindow().getShell().getDisplay();
BusyIndicator.showWhile(display, runnable);
} else
runnable.run();
}
}
/**
* Selects the given range on the editor.
*
* @param newOffset
* the selection offset
* @param newLength
* the selection range
*/
private void selectAndReveal(int newOffset, int newLength) {
Assert.isTrue(newOffset >= 0);
Assert.isTrue(newLength >= 0);
ITextEditor editor = getTextEditor();
if (editor instanceof PHPEditor) {
ISourceViewer viewer = ((PHPEditor) editor).getViewer();
if (viewer != null)
viewer.setSelectedRange(newOffset, newLength);
} else
// this is too intrusive, but will never get called anyway
getTextEditor().selectAndReveal(newOffset, newLength);
}
/**
* Indents a single line using the java heuristic scanner. Javadoc and
* multiline comments are indented as specified by the
* JavaDocAutoIndentStrategy
.
*
* @param document
* the document
* @param line
* the line to be indented
* @param caret
* the caret position
* @param indenter
* the java indenter
* @param scanner
* the heuristic scanner
* @return true
if document
was modified,
* false
otherwise
* @throws BadLocationException
* if the document got changed concurrently
*/
private boolean indentLine(IDocument document, int line, int caret,
JavaIndenter indenter, JavaHeuristicScanner scanner)
throws BadLocationException {
IRegion currentLine = document.getLineInformation(line);
int offset = currentLine.getOffset();
int wsStart = offset; // where we start searching for non-WS; after
// the "//" in single line comments
String indent = null;
if (offset < document.getLength()) {
ITypedRegion partition = TextUtilities.getPartition(document,
IPHPPartitions.PHP_PARTITIONING, offset, true);
String type = partition.getType();
if (type.equals(IPHPPartitions.PHP_PHPDOC_COMMENT)
|| type.equals(IPHPPartitions.PHP_MULTILINE_COMMENT)) {
// TODO this is a hack
// what I want to do
// new JavaDocAutoIndentStrategy().indentLineAtOffset(document,
// offset);
// return;
int start = 0;
if (line > 0) {
IRegion previousLine = document
.getLineInformation(line - 1);
start = previousLine.getOffset() + previousLine.getLength();
}
DocumentCommand command = new DocumentCommand() {
};
command.text = "\n"; //$NON-NLS-1$
command.offset = start;
new JavaDocAutoIndentStrategy(IPHPPartitions.PHP_PARTITIONING)
.customizeDocumentCommand(document, command);
int to = 1;
while (to < command.text.length()
&& Character.isWhitespace(command.text.charAt(to)))
to++;
indent = command.text.substring(1, to);
// omit Java style
// } else if (!fIsTabAction && partition.getOffset() == offset
// && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
//
// // line comment starting at position 0 -> indent inside
// int slashes = 2;
// while (slashes < document.getLength() - 1
// && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
// slashes += 2;
//
// wsStart = offset + slashes;
//
// StringBuffer computed = indenter.computeIndentation(offset);
// int tabSize = PHPeclipsePlugin
// .getDefault()
// .getPreferenceStore()
// .getInt(
// AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
// while (slashes > 0 && computed.length() > 0) {
// char c = computed.charAt(0);
// if (c == '\t')
// if (slashes > tabSize)
// slashes -= tabSize;
// else
// break;
// else if (c == ' ')
// slashes--;
// else
// break;
//
// computed.deleteCharAt(0);
// }
//
// indent = document.get(offset, wsStart - offset) + computed;
}
}
// standard java indentation
if (indent == null) {
StringBuffer computed = indenter.computeIndentation(offset);
if (computed != null)
indent = computed.toString();
else
//indent = new String();
return true; // prevent affecting html part
}
// change document:
// get current white space
int lineLength = currentLine.getLength();
int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart,
offset + lineLength);
if (end == JavaHeuristicScanner.NOT_FOUND)
end = offset + lineLength;
int length = end - offset;
String currentIndent = document.get(offset, length);
// if we are right before the text start / line end, and already after
// the insertion point
// then just insert a tab.
if (fIsTabAction && caret == end
&& whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
String tab = getTabEquivalent();
document.replace(caret, 0, tab);
fCaretOffset = caret + tab.length();
return true;
}
// set the caret offset so it can be used when setting the selection
if (caret >= offset && caret <= end)
fCaretOffset = offset + indent.length();
else
fCaretOffset = -1;
// only change the document if it is a real change
if (!indent.equals(currentIndent)) {
String deletedText = document.get(offset, length);
document.replace(offset, length, indent);
if (fIsTabAction
&& indent.length() > currentIndent.length()
&& PHPeclipsePlugin.getDefault().getPreferenceStore()
.getBoolean(
PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
ITextEditor editor = getTextEditor();
if (editor != null) {
final SmartBackspaceManager manager = (SmartBackspaceManager) editor
.getAdapter(SmartBackspaceManager.class);
if (manager != null) {
try {
// restore smart portion
ReplaceEdit smart = new ReplaceEdit(offset, indent
.length(), deletedText);
final UndoSpec spec = new UndoSpec(offset
+ indent.length(), new Region(caret, 0),
new TextEdit[] { smart }, 2, null);
manager.register(spec);
} catch (MalformedTreeException e) {
// log & ignore
PHPeclipsePlugin.log(new Status(IStatus.ERROR,
PHPeclipsePlugin.getPluginId(), IStatus.OK,
"Illegal smart backspace action", e)); //$NON-NLS-1$
}
}
}
}
return true;
} else
return false;
}
/**
* Returns the size in characters of a string. All characters count one,
* tabs count the editor's preference for the tab display
*
* @param indent
* the string to be measured.
* @return
*/
private int whiteSpaceLength(String indent) {
if (indent == null)
return 0;
else {
int size = 0;
int l = indent.length();
int tabSize = PHPeclipsePlugin.getDefault().getPreferenceStore().getInt(
AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
for (int i = 0; i < l; i++)
size += indent.charAt(i) == '\t' ? tabSize : 1;
return size;
}
}
/**
* Returns a tab equivalent, either as a tab character or as spaces,
* depending on the editor and formatter preferences.
*
* @return a string representing one tab in the editor, never
* null
*/
private String getTabEquivalent() {
String tab;
if (PHPeclipsePlugin.getDefault().getPreferenceStore().getBoolean(
PreferenceConstants.EDITOR_SPACES_FOR_TABS)) {
int size = JavaCore.getPlugin().getPluginPreferences().getInt(
DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE);
StringBuffer buf = new StringBuffer();
for (int i = 0; i < size; i++)
buf.append(' ');
tab = buf.toString();
} else
tab = "\t"; //$NON-NLS-1$
return tab;
}
/**
* Returns the editor's selection provider.
*
* @return the editor's selection provider or null
*/
private ISelectionProvider getSelectionProvider() {
ITextEditor editor = getTextEditor();
if (editor != null) {
return editor.getSelectionProvider();
}
return null;
}
/*
* @see org.eclipse.ui.texteditor.IUpdate#update()
*/
public void update() {
super.update();
if (isEnabled())
if (fIsTabAction)
setEnabled(canModifyEditor() && isSmartMode()
&& isValidSelection());
else
setEnabled(canModifyEditor() && !getSelection().isEmpty());
}
/**
* Returns if the current selection is valid, i.e. whether it is empty and
* the caret in the whitespace at the start of a line, or covers multiple
* lines.
*
* @return true
if the selection is valid for an indent
* operation
*/
private boolean isValidSelection() {
ITextSelection selection = getSelection();
if (selection.isEmpty()) {
return false;
}
int offset = selection.getOffset();
int length = selection.getLength();
IDocument document = getDocument();
if (document == null) {
return false;
}
try {
IRegion firstLine = document.getLineInformationOfOffset(offset);
int lineOffset = firstLine.getOffset();
// either the selection has to be empty and the caret in the WS at
// the line start
// or the selection has to extend over multiple lines
if (length == 0) {
boolean bRet;
bRet = document.get (lineOffset, offset - lineOffset).trim().length() == 0;
return bRet;
}
else {
// return lineOffset + firstLine.getLength() < offset + length;
return false; // only enable for empty selections for now
}
} catch (BadLocationException e) {
}
return false;
}
/**
* Returns the smart preference state.
*
* @return true
if smart mode is on, false
* otherwise
*/
private boolean isSmartMode() {
ITextEditor editor = getTextEditor();
if (editor instanceof ITextEditorExtension3)
return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;
return false;
}
/**
* Returns the document currently displayed in the editor, or
* null
if none can be obtained.
*
* @return the current document or null
*/
private IDocument getDocument() { ITextEditor editor = getTextEditor();
if (editor != null) {
IDocumentProvider provider = editor.getDocumentProvider();
IEditorInput input = editor.getEditorInput();
if (provider != null && input != null) {
return provider.getDocument(input);
}
}
return null;
}
/**
* Returns the selection on the editor or an invalid selection if none can
* be obtained. Returns never null
.
*
* @return the current selection, never null
*/
private ITextSelection getSelection() {
ISelectionProvider provider = getSelectionProvider();
if (provider != null) {
ISelection selection = provider.getSelection();
if (selection instanceof ITextSelection)
return (ITextSelection) selection;
}
// null object
return TextSelection.emptySelection();
}
}