X-Git-Url: http://secure.phpeclipse.com diff --git a/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/actions/IndentAction.java b/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/actions/IndentAction.java new file mode 100644 index 0000000..cf03914 --- /dev/null +++ b/net.sourceforge.phpeclipse/src/net/sourceforge/phpdt/internal/ui/actions/IndentAction.java @@ -0,0 +1,574 @@ +/******************************************************************************* + * 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(); + } + +}