Fixed: malfunctioned "Remove trailing spaces on editor save"
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / actions / IndentAction.java
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
7  * 
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.actions;
12
13 import java.util.ResourceBundle;
14
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;
26
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;
55
56 /**
57  * Indents a line or range of lines in a Java document to its correct position.
58  * No complete AST must be present, the indentation is computed using
59  * heuristics. The algorith used is fast for single lines, but does not store
60  * any information and therefore not so efficient for large line ranges.
61  * 
62  * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner
63  * @see net.sourceforge.phpdt.internal.ui.text.JavaIndenter
64  * @since 3.0
65  */
66 public class IndentAction extends TextEditorAction {
67
68         /** The caret offset after an indent operation. */
69         private int fCaretOffset;
70
71         /**
72          * Whether this is the action invoked by TAB. When <code>true</code>,
73          * indentation behaves differently to accomodate normal TAB operation.
74          */
75         private final boolean fIsTabAction;
76
77         /**
78          * Creates a new instance.
79          * 
80          * @param bundle
81          *            the resource bundle
82          * @param prefix
83          *            the prefix to use for keys in <code>bundle</code>
84          * @param editor
85          *            the text editor
86          * @param isTabAction
87          *            whether the action should insert tabs if over the indentation
88          */
89         public IndentAction(ResourceBundle bundle, String prefix,
90                         ITextEditor editor, boolean isTabAction) {
91                 super(bundle, prefix, editor);
92                 fIsTabAction = isTabAction;
93         }
94
95         /*
96          * @see org.eclipse.jface.action.Action#run()
97          */
98         public void run() {
99                 // update has been called by the framework
100                 if (!isEnabled() || !validateEditorInputState())
101                         return;
102
103                 ITextSelection selection = getSelection();
104                 final IDocument document = getDocument();
105
106                 if (document != null) {
107
108                         final int offset = selection.getOffset();
109                         final int length = selection.getLength();
110                         final Position end = new Position(offset + length);
111                         final int firstLine, nLines;
112                         fCaretOffset = -1;
113
114                         try {
115                                 document.addPosition(end);
116                                 firstLine = document.getLineOfOffset(offset);
117                                 // check for marginal (zero-length) lines
118                                 int minusOne = length == 0 ? 0 : 1;
119                                 nLines = document.getLineOfOffset(offset + length - minusOne)
120                                                 - firstLine + 1;
121                         } catch (BadLocationException e) {
122                                 // will only happen on concurrent modification
123                                 PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin
124                                                 .getPluginId(), IStatus.OK, "", e)); //$NON-NLS-1$
125                                 return;
126                         }
127
128                         Runnable runnable = new Runnable() {
129                                 public void run() {
130                                         IRewriteTarget target = (IRewriteTarget) getTextEditor()
131                                                         .getAdapter(IRewriteTarget.class);
132                                         if (target != null) {
133                                                 target.beginCompoundChange();
134                                                 target.setRedraw(false);
135                                         }
136
137                                         try {
138                                                 JavaHeuristicScanner scanner = new JavaHeuristicScanner(
139                                                                 document);
140                                                 JavaIndenter indenter = new JavaIndenter(document,
141                                                                 scanner);
142                                                 boolean hasChanged = false;
143                                                 for (int i = 0; i < nLines; i++) {
144                                                         hasChanged |= indentLine(document, firstLine + i,
145                                                                         offset, indenter, scanner);
146                                                 }
147
148                                                 // update caret position: move to new position when
149                                                 // indenting just one line
150                                                 // keep selection when indenting multiple
151                                                 int newOffset, newLength;
152                                                 if (fIsTabAction) {
153                                                         newOffset = fCaretOffset;
154                                                         newLength = 0;
155                                                 } else if (nLines > 1) {
156                                                         newOffset = offset;
157                                                         newLength = end.getOffset() - offset;
158                                                 } else {
159                                                         newOffset = fCaretOffset;
160                                                         newLength = 0;
161                                                 }
162
163                                                 // always reset the selection if anything was replaced
164                                                 // but not when we had a singleline nontab invocation
165                                                 if (newOffset != -1
166                                                                 && (hasChanged || newOffset != offset || newLength != length))
167                                                         selectAndReveal(newOffset, newLength);
168
169                                                 document.removePosition(end);
170                                         } catch (BadLocationException e) {
171                                                 // will only happen on concurrent modification
172                                                 PHPeclipsePlugin.log(new Status(IStatus.ERROR,
173                                                                 PHPeclipsePlugin.getPluginId(), IStatus.OK,
174                                                                 "ConcurrentModification in IndentAction", e)); //$NON-NLS-1$
175
176                                         } finally {
177
178                                                 if (target != null) {
179                                                         target.endCompoundChange();
180                                                         target.setRedraw(true);
181                                                 }
182                                         }
183                                 }
184                         };
185
186                         if (nLines > 50) {
187                                 Display display = getTextEditor().getEditorSite()
188                                                 .getWorkbenchWindow().getShell().getDisplay();
189                                 BusyIndicator.showWhile(display, runnable);
190                         } else
191                                 runnable.run();
192
193                 }
194         }
195
196         /**
197          * Selects the given range on the editor.
198          * 
199          * @param newOffset
200          *            the selection offset
201          * @param newLength
202          *            the selection range
203          */
204         private void selectAndReveal(int newOffset, int newLength) {
205                 Assert.isTrue(newOffset >= 0);
206                 Assert.isTrue(newLength >= 0);
207                 ITextEditor editor = getTextEditor();
208                 if (editor instanceof PHPEditor) {
209                         ISourceViewer viewer = ((PHPEditor) editor).getViewer();
210                         if (viewer != null)
211                                 viewer.setSelectedRange(newOffset, newLength);
212                 } else
213                         // this is too intrusive, but will never get called anyway
214                         getTextEditor().selectAndReveal(newOffset, newLength);
215
216         }
217
218         /**
219          * Indents a single line using the java heuristic scanner. Javadoc and
220          * multiline comments are indented as specified by the
221          * <code>JavaDocAutoIndentStrategy</code>.
222          * 
223          * @param document
224          *            the document
225          * @param line
226          *            the line to be indented
227          * @param caret
228          *            the caret position
229          * @param indenter
230          *            the java indenter
231          * @param scanner
232          *            the heuristic scanner
233          * @return <code>true</code> if <code>document</code> was modified,
234          *         <code>false</code> otherwise
235          * @throws BadLocationException
236          *             if the document got changed concurrently
237          */
238         private boolean indentLine(IDocument document, int line, int caret,
239                         JavaIndenter indenter, JavaHeuristicScanner scanner)
240                         throws BadLocationException {
241                 IRegion currentLine = document.getLineInformation(line);
242                 int offset = currentLine.getOffset();
243                 int wsStart = offset; // where we start searching for non-WS; after
244                                                                 // the "//" in single line comments
245
246                 String indent = null;
247                 if (offset < document.getLength()) {
248                         ITypedRegion partition = TextUtilities.getPartition(document,
249                                         IPHPPartitions.PHP_PARTITIONING, offset, true);
250                         String type = partition.getType();
251                         if (type.equals(IPHPPartitions.PHP_PHPDOC_COMMENT)
252                                         || type.equals(IPHPPartitions.PHP_MULTILINE_COMMENT)) {
253
254                                 // TODO this is a hack
255                                 // what I want to do
256                                 // new JavaDocAutoIndentStrategy().indentLineAtOffset(document,
257                                 // offset);
258                                 // return;
259
260                                 int start = 0;
261                                 if (line > 0) {
262
263                                         IRegion previousLine = document
264                                                         .getLineInformation(line - 1);
265                                         start = previousLine.getOffset() + previousLine.getLength();
266                                 }
267
268                                 DocumentCommand command = new DocumentCommand() {
269                                 };
270                                 command.text = "\n"; //$NON-NLS-1$
271                                 command.offset = start;
272                                 new JavaDocAutoIndentStrategy(IPHPPartitions.PHP_PARTITIONING)
273                                                 .customizeDocumentCommand(document, command);
274                                 int to = 1;
275                                 while (to < command.text.length()
276                                                 && Character.isWhitespace(command.text.charAt(to)))
277                                         to++;
278                                 indent = command.text.substring(1, to);
279
280                         } else if (!fIsTabAction && partition.getOffset() == offset
281                                         && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
282
283                                 // line comment starting at position 0 -> indent inside
284                                 int slashes = 2;
285                                 while (slashes < document.getLength() - 1
286                                                 && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
287                                         slashes += 2;
288
289                                 wsStart = offset + slashes;
290
291                                 StringBuffer computed = indenter.computeIndentation(offset);
292                                 int tabSize = PHPeclipsePlugin
293                                                 .getDefault()
294                                                 .getPreferenceStore()
295                                                 .getInt(
296                                                                 AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
297                                 while (slashes > 0 && computed.length() > 0) {
298                                         char c = computed.charAt(0);
299                                         if (c == '\t')
300                                                 if (slashes > tabSize)
301                                                         slashes -= tabSize;
302                                                 else
303                                                         break;
304                                         else if (c == ' ')
305                                                 slashes--;
306                                         else
307                                                 break;
308
309                                         computed.deleteCharAt(0);
310                                 }
311
312                                 indent = document.get(offset, wsStart - offset) + computed;
313
314                         }
315                 }
316
317                 // standard java indentation
318                 if (indent == null) {
319                         StringBuffer computed = indenter.computeIndentation(offset);
320                         if (computed != null)
321                                 indent = computed.toString();
322                         else
323                                 indent = new String();
324                 }
325
326                 // change document:
327                 // get current white space
328                 int lineLength = currentLine.getLength();
329                 int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart,
330                                 offset + lineLength);
331                 if (end == JavaHeuristicScanner.NOT_FOUND)
332                         end = offset + lineLength;
333                 int length = end - offset;
334                 String currentIndent = document.get(offset, length);
335
336                 // if we are right before the text start / line end, and already after
337                 // the insertion point
338                 // then just insert a tab.
339                 if (fIsTabAction && caret == end
340                                 && whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
341                         String tab = getTabEquivalent();
342                         document.replace(caret, 0, tab);
343                         fCaretOffset = caret + tab.length();
344                         return true;
345                 }
346
347                 // set the caret offset so it can be used when setting the selection
348                 if (caret >= offset && caret <= end)
349                         fCaretOffset = offset + indent.length();
350                 else
351                         fCaretOffset = -1;
352
353                 // only change the document if it is a real change
354                 if (!indent.equals(currentIndent)) {
355                         String deletedText = document.get(offset, length);
356                         document.replace(offset, length, indent);
357
358                         if (fIsTabAction
359                                         && indent.length() > currentIndent.length()
360                                         && PHPeclipsePlugin.getDefault().getPreferenceStore()
361                                                         .getBoolean(
362                                                                         PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
363                                 ITextEditor editor = getTextEditor();
364                                 if (editor != null) {
365                                         final SmartBackspaceManager manager = (SmartBackspaceManager) editor
366                                                         .getAdapter(SmartBackspaceManager.class);
367                                         if (manager != null) {
368                                                 try {
369                                                         // restore smart portion
370                                                         ReplaceEdit smart = new ReplaceEdit(offset, indent
371                                                                         .length(), deletedText);
372
373                                                         final UndoSpec spec = new UndoSpec(offset
374                                                                         + indent.length(), new Region(caret, 0),
375                                                                         new TextEdit[] { smart }, 2, null);
376                                                         manager.register(spec);
377                                                 } catch (MalformedTreeException e) {
378                                                         // log & ignore
379                                                         PHPeclipsePlugin.log(new Status(IStatus.ERROR,
380                                                                         PHPeclipsePlugin.getPluginId(), IStatus.OK,
381                                                                         "Illegal smart backspace action", e)); //$NON-NLS-1$
382                                                 }
383                                         }
384                                 }
385                         }
386
387                         return true;
388                 } else
389                         return false;
390         }
391
392         /**
393          * Returns the size in characters of a string. All characters count one,
394          * tabs count the editor's preference for the tab display
395          * 
396          * @param indent
397          *            the string to be measured.
398          * @return
399          */
400         private int whiteSpaceLength(String indent) {
401                 if (indent == null)
402                         return 0;
403                 else {
404                         int size = 0;
405                         int l = indent.length();
406                         int tabSize = PHPeclipsePlugin
407                                         .getDefault()
408                                         .getPreferenceStore()
409                                         .getInt(
410                                                         AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
411
412                         for (int i = 0; i < l; i++)
413                                 size += indent.charAt(i) == '\t' ? tabSize : 1;
414                         return size;
415                 }
416         }
417
418         /**
419          * Returns a tab equivalent, either as a tab character or as spaces,
420          * depending on the editor and formatter preferences.
421          * 
422          * @return a string representing one tab in the editor, never
423          *         <code>null</code>
424          */
425         private String getTabEquivalent() {
426                 String tab;
427                 if (PHPeclipsePlugin.getDefault().getPreferenceStore().getBoolean(
428                                 PreferenceConstants.EDITOR_SPACES_FOR_TABS)) {
429                         int size = JavaCore.getPlugin().getPluginPreferences().getInt(
430                                         DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE);
431                         StringBuffer buf = new StringBuffer();
432                         for (int i = 0; i < size; i++)
433                                 buf.append(' ');
434                         tab = buf.toString();
435                 } else
436                         tab = "\t"; //$NON-NLS-1$
437
438                 return tab;
439         }
440
441         /**
442          * Returns the editor's selection provider.
443          * 
444          * @return the editor's selection provider or <code>null</code>
445          */
446         private ISelectionProvider getSelectionProvider() {
447                 ITextEditor editor = getTextEditor();
448                 if (editor != null) {
449                         return editor.getSelectionProvider();
450                 }
451                 return null;
452         }
453
454         /*
455          * @see org.eclipse.ui.texteditor.IUpdate#update()
456          */
457         public void update() {
458                 super.update();
459
460                 if (isEnabled())
461                         if (fIsTabAction)
462                                 setEnabled(canModifyEditor() && isSmartMode()
463                                                 && isValidSelection());
464                         else
465                                 setEnabled(canModifyEditor() && !getSelection().isEmpty());
466         }
467
468         /**
469          * Returns if the current selection is valid, i.e. whether it is empty and
470          * the caret in the whitespace at the start of a line, or covers multiple
471          * lines.
472          * 
473          * @return <code>true</code> if the selection is valid for an indent
474          *         operation
475          */
476         private boolean isValidSelection() {
477                 ITextSelection selection = getSelection();
478                 if (selection.isEmpty())
479                         return false;
480
481                 int offset = selection.getOffset();
482                 int length = selection.getLength();
483
484                 IDocument document = getDocument();
485                 if (document == null)
486                         return false;
487
488                 try {
489                         IRegion firstLine = document.getLineInformationOfOffset(offset);
490                         int lineOffset = firstLine.getOffset();
491
492                         // either the selection has to be empty and the caret in the WS at
493                         // the line start
494                         // or the selection has to extend over multiple lines
495                         if (length == 0)
496                                 return document.get(lineOffset, offset - lineOffset).trim()
497                                                 .length() == 0;
498                         else
499                                 // return lineOffset + firstLine.getLength() < offset + length;
500                                 return false; // only enable for empty selections for now
501
502                 } catch (BadLocationException e) {
503                 }
504
505                 return false;
506         }
507
508         /**
509          * Returns the smart preference state.
510          * 
511          * @return <code>true</code> if smart mode is on, <code>false</code>
512          *         otherwise
513          */
514         private boolean isSmartMode() {
515                 ITextEditor editor = getTextEditor();
516
517                 if (editor instanceof ITextEditorExtension3)
518                         return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;
519
520                 return false;
521         }
522
523         /**
524          * Returns the document currently displayed in the editor, or
525          * <code>null</code> if none can be obtained.
526          * 
527          * @return the current document or <code>null</code>
528          */
529         private IDocument getDocument() {
530
531                 ITextEditor editor = getTextEditor();
532                 if (editor != null) {
533
534                         IDocumentProvider provider = editor.getDocumentProvider();
535                         IEditorInput input = editor.getEditorInput();
536                         if (provider != null && input != null)
537                                 return provider.getDocument(input);
538
539                 }
540                 return null;
541         }
542
543         /**
544          * Returns the selection on the editor or an invalid selection if none can
545          * be obtained. Returns never <code>null</code>.
546          * 
547          * @return the current selection, never <code>null</code>
548          */
549         private ITextSelection getSelection() {
550                 ISelectionProvider provider = getSelectionProvider();
551                 if (provider != null) {
552
553                         ISelection selection = provider.getSelection();
554                         if (selection instanceof ITextSelection)
555                                 return (ITextSelection) selection;
556                 }
557
558                 // null object
559                 return TextSelection.emptySelection();
560         }
561
562 }