1 /*******************************************************************************
 
   2  * Copyright (c) 2000, 2004 IBM Corporation and others.
 
   3  * All rights reserved. This program and the accompanying materials 
 
   4  * are made available under the terms of the Common Public License v1.0
 
   5  * which accompanies this distribution, and is available at
 
   6  * http://www.eclipse.org/legal/cpl-v10.html
 
   9  *     IBM Corporation - initial API and implementation
 
  10  *******************************************************************************/
 
  11 package net.sourceforge.phpdt.internal.ui.actions;
 
  13 import java.util.ResourceBundle;
 
  15 import net.sourceforge.phpdt.core.JavaCore;
 
  16 import net.sourceforge.phpdt.core.formatter.DefaultCodeFormatterConstants;
 
  17 import net.sourceforge.phpdt.internal.ui.text.IPHPPartitions;
 
  18 import net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner;
 
  19 import net.sourceforge.phpdt.internal.ui.text.JavaIndenter;
 
  20 import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager;
 
  21 import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
 
  22 import net.sourceforge.phpdt.internal.ui.text.phpdoc.JavaDocAutoIndentStrategy;
 
  23 import net.sourceforge.phpdt.ui.PreferenceConstants;
 
  24 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
 
  25 import net.sourceforge.phpeclipse.phpeditor.PHPEditor;
 
  27 import org.eclipse.core.runtime.IStatus;
 
  28 import org.eclipse.core.runtime.Status;
 
  29 import org.eclipse.jface.text.Assert;
 
  30 import org.eclipse.jface.text.BadLocationException;
 
  31 import org.eclipse.jface.text.DocumentCommand;
 
  32 import org.eclipse.jface.text.IDocument;
 
  33 import org.eclipse.jface.text.IRegion;
 
  34 import org.eclipse.jface.text.IRewriteTarget;
 
  35 import org.eclipse.jface.text.ITextSelection;
 
  36 import org.eclipse.jface.text.ITypedRegion;
 
  37 import org.eclipse.jface.text.Position;
 
  38 import org.eclipse.jface.text.Region;
 
  39 import org.eclipse.jface.text.TextSelection;
 
  40 import org.eclipse.jface.text.TextUtilities;
 
  41 import org.eclipse.jface.text.source.ISourceViewer;
 
  42 import org.eclipse.jface.viewers.ISelection;
 
  43 import org.eclipse.jface.viewers.ISelectionProvider;
 
  44 import org.eclipse.swt.custom.BusyIndicator;
 
  45 import org.eclipse.swt.widgets.Display;
 
  46 import org.eclipse.text.edits.MalformedTreeException;
 
  47 import org.eclipse.text.edits.ReplaceEdit;
 
  48 import org.eclipse.text.edits.TextEdit;
 
  49 import org.eclipse.ui.IEditorInput;
 
  50 import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
 
  51 import org.eclipse.ui.texteditor.IDocumentProvider;
 
  52 import org.eclipse.ui.texteditor.ITextEditor;
 
  53 import org.eclipse.ui.texteditor.ITextEditorExtension3;
 
  54 import org.eclipse.ui.texteditor.TextEditorAction;
 
  58  * Indents a line or range of lines in a Java document to its correct position. No complete
 
  59  * AST must be present, the indentation is computed using heuristics. The algorith used is fast for
 
  60  * single lines, but does not store any information and therefore not so efficient for large line
 
  63  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner
 
  64  * @see net.sourceforge.phpdt.internal.ui.text.JavaIndenter
 
  67 public class IndentAction extends TextEditorAction {
 
  69         /** The caret offset after an indent operation. */
 
  70         private int fCaretOffset;
 
  73          * Whether this is the action invoked by TAB. When <code>true</code>, indentation behaves 
 
  74          * differently to accomodate normal TAB operation.
 
  76         private final boolean fIsTabAction;
 
  79          * Creates a new instance.
 
  81          * @param bundle the resource bundle
 
  82          * @param prefix the prefix to use for keys in <code>bundle</code>
 
  83          * @param editor the text editor
 
  84          * @param isTabAction whether the action should insert tabs if over the indentation
 
  86         public IndentAction(ResourceBundle bundle, String prefix, ITextEditor editor, boolean isTabAction) {
 
  87                 super(bundle, prefix, editor);
 
  88                 fIsTabAction= isTabAction;
 
  92          * @see org.eclipse.jface.action.Action#run()
 
  95                 // update has been called by the framework
 
  96                 if (!isEnabled() || !validateEditorInputState())
 
  99                 ITextSelection selection= getSelection();
 
 100                 final IDocument document= getDocument();
 
 102                 if (document != null) {
 
 104                         final int offset= selection.getOffset();
 
 105                         final int length= selection.getLength();
 
 106                         final Position end= new Position(offset + length);
 
 107                         final int firstLine, nLines;
 
 111                                 document.addPosition(end);
 
 112                                 firstLine= document.getLineOfOffset(offset);
 
 113                                 // check for marginal (zero-length) lines
 
 114                                 int minusOne= length == 0 ? 0 : 1;
 
 115                                 nLines= document.getLineOfOffset(offset + length - minusOne) - firstLine + 1;
 
 116                         } catch (BadLocationException e) {
 
 117                                 // will only happen on concurrent modification
 
 118                           PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin.getPluginId(), IStatus.OK, "", e)); //$NON-NLS-1$
 
 122                         Runnable runnable= new Runnable() {
 
 124                                         IRewriteTarget target= (IRewriteTarget)getTextEditor().getAdapter(IRewriteTarget.class);
 
 125                                         if (target != null) {
 
 126                                                 target.beginCompoundChange();
 
 127                                                 target.setRedraw(false);
 
 131                                                 JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
 
 132                                                 JavaIndenter indenter= new JavaIndenter(document, scanner);
 
 133                                                 boolean hasChanged= false;
 
 134                                                 for (int i= 0; i < nLines; i++) {
 
 135                                                         hasChanged |= indentLine(document, firstLine + i, offset, indenter, scanner);
 
 138                                                 // update caret position: move to new position when indenting just one line
 
 139                                                 // keep selection when indenting multiple
 
 140                                                 int newOffset, newLength;
 
 142                                                         newOffset= fCaretOffset;
 
 144                                                 } else if (nLines > 1) {
 
 146                                                         newLength= end.getOffset() - offset;
 
 148                                                         newOffset= fCaretOffset;
 
 152                                                 // always reset the selection if anything was replaced
 
 153                                                 // but not when we had a singleline nontab invocation
 
 154                                                 if (newOffset != -1 && (hasChanged || newOffset != offset || newLength != length))
 
 155                                                         selectAndReveal(newOffset, newLength);
 
 157                                                 document.removePosition(end);
 
 158                                         } catch (BadLocationException e) {
 
 159                                                 // will only happen on concurrent modification
 
 160                                           PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin.getPluginId(), IStatus.OK, "ConcurrentModification in IndentAction", e)); //$NON-NLS-1$
 
 164                                                 if (target != null) {
 
 165                                                         target.endCompoundChange();
 
 166                                                         target.setRedraw(true);
 
 173                                 Display display= getTextEditor().getEditorSite().getWorkbenchWindow().getShell().getDisplay();
 
 174                                 BusyIndicator.showWhile(display, runnable);
 
 182          * Selects the given range on the editor.
 
 184          * @param newOffset the selection offset
 
 185          * @param newLength the selection range
 
 187         private void selectAndReveal(int newOffset, int newLength) {
 
 188                 Assert.isTrue(newOffset >= 0); 
 
 189                 Assert.isTrue(newLength >= 0); 
 
 190                 ITextEditor editor= getTextEditor();
 
 191                 if (editor instanceof PHPEditor) {
 
 192                         ISourceViewer viewer= ((PHPEditor)editor).getViewer();
 
 194                                 viewer.setSelectedRange(newOffset, newLength);
 
 196                         // this is too intrusive, but will never get called anyway
 
 197                         getTextEditor().selectAndReveal(newOffset, newLength);
 
 202          * Indents a single line using the java heuristic scanner. Javadoc and multiline comments are 
 
 203          * indented as specified by the <code>JavaDocAutoIndentStrategy</code>.
 
 205          * @param document the document
 
 206          * @param line the line to be indented
 
 207          * @param caret the caret position
 
 208          * @param indenter the java indenter
 
 209          * @param scanner the heuristic scanner
 
 210          * @return <code>true</code> if <code>document</code> was modified, <code>false</code> otherwise
 
 211          * @throws BadLocationException if the document got changed concurrently 
 
 213         private boolean indentLine(IDocument document, int line, int caret, JavaIndenter indenter, JavaHeuristicScanner scanner) throws BadLocationException {
 
 214                 IRegion currentLine= document.getLineInformation(line);
 
 215                 int offset= currentLine.getOffset();
 
 216                 int wsStart= offset; // where we start searching for non-WS; after the "//" in single line comments
 
 219                 if (offset < document.getLength()) {
 
 220                         ITypedRegion partition= TextUtilities.getPartition(document, IPHPPartitions.PHP_PARTITIONING, offset, true);
 
 221                         String type= partition.getType();
 
 222                         if (type.equals(IPHPPartitions.PHP_PHPDOC_COMMENT) || type.equals(IPHPPartitions.PHP_MULTILINE_COMMENT)) {
 
 224                                 // TODO this is a hack
 
 226 //                              new JavaDocAutoIndentStrategy().indentLineAtOffset(document, offset);
 
 232                                         IRegion previousLine= document.getLineInformation(line - 1);
 
 233                                         start= previousLine.getOffset() + previousLine.getLength();
 
 236                                 DocumentCommand command= new DocumentCommand() {};
 
 237                                 command.text= "\n"; //$NON-NLS-1$
 
 238                                 command.offset= start;
 
 239                                 new JavaDocAutoIndentStrategy(IPHPPartitions.PHP_PARTITIONING).customizeDocumentCommand(document, command);
 
 241                                 while (to < command.text.length() && Character.isWhitespace(command.text.charAt(to)))
 
 243                                 indent= command.text.substring(1, to);
 
 245                         } else if (!fIsTabAction && partition.getOffset() == offset && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
 
 247                                 // line comment starting at position 0 -> indent inside
 
 249                                 while (slashes < document.getLength() - 1 && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
 
 252                                 wsStart= offset + slashes;
 
 254                                 StringBuffer computed= indenter.computeIndentation(offset);
 
 255                                 int tabSize= PHPeclipsePlugin.getDefault().getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
 
 256                                 while (slashes > 0 && computed.length() > 0) {
 
 257                                         char c= computed.charAt(0);
 
 259                                                 if (slashes > tabSize)
 
 267                                         computed.deleteCharAt(0);
 
 270                                 indent= document.get(offset, wsStart - offset) + computed;
 
 275                 // standard java indentation
 
 276                 if (indent == null) {
 
 277                         StringBuffer computed= indenter.computeIndentation(offset);
 
 278                         if (computed != null)
 
 279                                 indent= computed.toString();
 
 281                                 indent= new String();
 
 285                 // get current white space
 
 286                 int lineLength= currentLine.getLength();
 
 287                 int end= scanner.findNonWhitespaceForwardInAnyPartition(wsStart, offset + lineLength);
 
 288                 if (end == JavaHeuristicScanner.NOT_FOUND)
 
 289                         end= offset + lineLength;
 
 290                 int length= end - offset;
 
 291                 String currentIndent= document.get(offset, length);
 
 293                 // if we are right before the text start / line end, and already after the insertion point
 
 294                 // then just insert a tab.
 
 295                 if (fIsTabAction && caret == end && whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
 
 296                         String tab= getTabEquivalent();
 
 297                         document.replace(caret, 0, tab);
 
 298                         fCaretOffset= caret + tab.length();
 
 302                 // set the caret offset so it can be used when setting the selection
 
 303                 if (caret >= offset && caret <= end)
 
 304                         fCaretOffset= offset + indent.length();
 
 308                 // only change the document if it is a real change
 
 309                 if (!indent.equals(currentIndent)) {
 
 310                         String deletedText= document.get(offset, length);
 
 311                         document.replace(offset, length, indent);
 
 313                         if (fIsTabAction && indent.length() > currentIndent.length() && PHPeclipsePlugin.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
 
 314                                 ITextEditor editor= getTextEditor();
 
 315                                 if (editor != null) {
 
 316                                         final SmartBackspaceManager manager= (SmartBackspaceManager) editor.getAdapter(SmartBackspaceManager.class);
 
 317                                         if (manager != null) {
 
 319                                                         // restore smart portion
 
 320                                                         ReplaceEdit smart= new ReplaceEdit(offset, indent.length(), deletedText);
 
 322                                                         final UndoSpec spec= new UndoSpec(
 
 323                                                                         offset + indent.length(),
 
 324                                                                         new Region(caret, 0),
 
 325                                                                         new TextEdit[] { smart },
 
 328                                                         manager.register(spec);
 
 329                                                 } catch (MalformedTreeException e) {
 
 331                                                   PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin.getPluginId(), IStatus.OK, "Illegal smart backspace action", e)); //$NON-NLS-1$
 
 344          * Returns the size in characters of a string. All characters count one, tabs count the editor's
 
 345          * preference for the tab display 
 
 347          * @param indent the string to be measured.
 
 350         private int whiteSpaceLength(String indent) {
 
 355                         int l= indent.length();
 
 356                         int tabSize= PHPeclipsePlugin.getDefault().getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
 
 358                         for (int i= 0; i < l; i++)
 
 359                                 size += indent.charAt(i) == '\t' ? tabSize : 1;
 
 365          * Returns a tab equivalent, either as a tab character or as spaces, depending on the editor and
 
 366          * formatter preferences.
 
 368          * @return a string representing one tab in the editor, never <code>null</code>
 
 370         private String getTabEquivalent() {
 
 372                 if (PHPeclipsePlugin.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_SPACES_FOR_TABS)) {
 
 373                         int size= JavaCore.getPlugin().getPluginPreferences().getInt(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE);
 
 374                         StringBuffer buf= new StringBuffer();
 
 375                         for (int i= 0; i< size; i++)
 
 379                         tab= "\t"; //$NON-NLS-1$
 
 385          * Returns the editor's selection provider.
 
 387          * @return the editor's selection provider or <code>null</code>
 
 389         private ISelectionProvider getSelectionProvider() {
 
 390                 ITextEditor editor= getTextEditor();
 
 391                 if (editor != null) {
 
 392                         return editor.getSelectionProvider();
 
 398          * @see org.eclipse.ui.texteditor.IUpdate#update()
 
 400         public void update() {
 
 405                                 setEnabled(canModifyEditor() && isSmartMode() && isValidSelection());
 
 407                                 setEnabled(canModifyEditor() && !getSelection().isEmpty());
 
 411          * Returns if the current selection is valid, i.e. whether it is empty and the caret in the 
 
 412          * whitespace at the start of a line, or covers multiple lines.
 
 414          * @return <code>true</code> if the selection is valid for an indent operation
 
 416         private boolean isValidSelection() {
 
 417                 ITextSelection selection= getSelection();
 
 418                 if (selection.isEmpty())
 
 421                 int offset= selection.getOffset();
 
 422                 int length= selection.getLength();
 
 424                 IDocument document= getDocument();
 
 425                 if (document == null)
 
 429                         IRegion firstLine= document.getLineInformationOfOffset(offset);
 
 430                         int lineOffset= firstLine.getOffset();
 
 432                         // either the selection has to be empty and the caret in the WS at the line start
 
 433                         // or the selection has to extend over multiple lines
 
 435                                 return document.get(lineOffset, offset - lineOffset).trim().length() == 0;
 
 437 //                              return lineOffset + firstLine.getLength() < offset + length;
 
 438                                 return false; // only enable for empty selections for now
 
 440                 } catch (BadLocationException e) {
 
 447          * Returns the smart preference state.
 
 449          * @return <code>true</code> if smart mode is on, <code>false</code> otherwise
 
 451         private boolean isSmartMode() {
 
 452                 ITextEditor editor= getTextEditor();
 
 454                 if (editor instanceof ITextEditorExtension3)
 
 455                         return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;
 
 461          * Returns the document currently displayed in the editor, or <code>null</code> if none can be 
 
 464          * @return the current document or <code>null</code>
 
 466         private IDocument getDocument() {
 
 468                 ITextEditor editor= getTextEditor();
 
 469                 if (editor != null) {
 
 471                         IDocumentProvider provider= editor.getDocumentProvider();
 
 472                         IEditorInput input= editor.getEditorInput();
 
 473                         if (provider != null && input != null)
 
 474                                 return provider.getDocument(input);
 
 481          * Returns the selection on the editor or an invalid selection if none can be obtained. Returns
 
 482          * never <code>null</code>.
 
 484          * @return the current selection, never <code>null</code>
 
 486         private ITextSelection getSelection() {
 
 487                 ISelectionProvider provider= getSelectionProvider();
 
 488                 if (provider != null) {
 
 490                         ISelection selection= provider.getSelection();
 
 491                         if (selection instanceof ITextSelection)
 
 492                                 return (ITextSelection) selection;
 
 496                 return TextSelection.emptySelection();