1) Moved net.sourceforge.phpeclipse.ui\src\net\sourceforge\phpdt back to net.sourcefo...
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / actions / IndentAction.java
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 (file)
index 0000000..cf03914
--- /dev/null
@@ -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 <code>true</code>,
+        * 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 <code>bundle</code>
+        * @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
+        * <code>JavaDocAutoIndentStrategy</code>.
+        * 
+        * @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 <code>true</code> if <code>document</code> was modified,
+        *         <code>false</code> 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
+        *         <code>null</code>
+        */
+       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 <code>null</code>
+        */
+       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 <code>true</code> 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 <code>true</code> if smart mode is on, <code>false</code>
+        *         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
+        * <code>null</code> if none can be obtained.
+        * 
+        * @return the current document or <code>null</code>
+        */
+       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 <code>null</code>.
+        * 
+        * @return the current selection, never <code>null</code>
+        */
+       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();
+       }
+
+}