Fix nasty bug #706. See trac.
[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 // omit Java style
281 //                      } else if (!fIsTabAction && partition.getOffset() == offset
282 //                                      && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
283 //
284 //                              // line comment starting at position 0 -> indent inside
285 //                              int slashes = 2;
286 //                              while (slashes < document.getLength() - 1
287 //                                              && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
288 //                                      slashes += 2;
289 //
290 //                              wsStart = offset + slashes;
291 //
292 //                              StringBuffer computed = indenter.computeIndentation(offset);
293 //                              int tabSize = PHPeclipsePlugin
294 //                                              .getDefault()
295 //                                              .getPreferenceStore()
296 //                                              .getInt(
297 //                                                              AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
298 //                              while (slashes > 0 && computed.length() > 0) {
299 //                                      char c = computed.charAt(0);
300 //                                      if (c == '\t')
301 //                                              if (slashes > tabSize)
302 //                                                      slashes -= tabSize;
303 //                                              else
304 //                                                      break;
305 //                                      else if (c == ' ')
306 //                                              slashes--;
307 //                                      else
308 //                                              break;
309 //
310 //                                      computed.deleteCharAt(0);
311 //                              }
312 //
313 //                              indent = document.get(offset, wsStart - offset) + computed;
314
315                         }
316                 }
317
318                 // standard java indentation
319                 if (indent == null) {
320                         StringBuffer computed = indenter.computeIndentation(offset);
321                         if (computed != null)
322                                 indent = computed.toString();
323                         else
324                                 //indent = new String();
325                                 return true; // prevent affecting html part
326                 }
327
328                 // change document:
329                 // get current white space
330                 int lineLength = currentLine.getLength();
331                 int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart,
332                                 offset + lineLength);
333                 if (end == JavaHeuristicScanner.NOT_FOUND)
334                         end = offset + lineLength;
335                 int length = end - offset;
336                 String currentIndent = document.get(offset, length);
337
338                 // if we are right before the text start / line end, and already after
339                 // the insertion point
340                 // then just insert a tab.
341                 if (fIsTabAction && caret == end
342                                 && whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
343                         String tab = getTabEquivalent();
344                         document.replace(caret, 0, tab);
345                         fCaretOffset = caret + tab.length();
346                         return true;
347                 }
348
349                 // set the caret offset so it can be used when setting the selection
350                 if (caret >= offset && caret <= end)
351                         fCaretOffset = offset + indent.length();
352                 else
353                         fCaretOffset = -1;
354
355                 // only change the document if it is a real change
356                 if (!indent.equals(currentIndent)) {
357                         String deletedText = document.get(offset, length);
358                         document.replace(offset, length, indent);
359
360                         if (fIsTabAction
361                                         && indent.length() > currentIndent.length()
362                                         && PHPeclipsePlugin.getDefault().getPreferenceStore()
363                                                         .getBoolean(
364                                                                         PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
365                                 ITextEditor editor = getTextEditor();
366                                 if (editor != null) {
367                                         final SmartBackspaceManager manager = (SmartBackspaceManager) editor
368                                                         .getAdapter(SmartBackspaceManager.class);
369                                         if (manager != null) {
370                                                 try {
371                                                         // restore smart portion
372                                                         ReplaceEdit smart = new ReplaceEdit(offset, indent
373                                                                         .length(), deletedText);
374
375                                                         final UndoSpec spec = new UndoSpec(offset
376                                                                         + indent.length(), new Region(caret, 0),
377                                                                         new TextEdit[] { smart }, 2, null);
378                                                         manager.register(spec);
379                                                 } catch (MalformedTreeException e) {
380                                                         // log & ignore
381                                                         PHPeclipsePlugin.log(new Status(IStatus.ERROR,
382                                                                         PHPeclipsePlugin.getPluginId(), IStatus.OK,
383                                                                         "Illegal smart backspace action", e)); //$NON-NLS-1$
384                                                 }
385                                         }
386                                 }
387                         }
388
389                         return true;
390                 } else
391                         return false;
392         }
393
394         /**
395          * Returns the size in characters of a string. All characters count one,
396          * tabs count the editor's preference for the tab display
397          * 
398          * @param indent
399          *            the string to be measured.
400          * @return
401          */
402         private int whiteSpaceLength(String indent) {
403                 if (indent == null)
404                         return 0;
405                 else {
406                         int size = 0;
407                         int l = indent.length();
408                         int tabSize = PHPeclipsePlugin
409                                         .getDefault()
410                                         .getPreferenceStore()
411                                         .getInt(
412                                                         AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
413
414                         for (int i = 0; i < l; i++)
415                                 size += indent.charAt(i) == '\t' ? tabSize : 1;
416                         return size;
417                 }
418         }
419
420         /**
421          * Returns a tab equivalent, either as a tab character or as spaces,
422          * depending on the editor and formatter preferences.
423          * 
424          * @return a string representing one tab in the editor, never
425          *         <code>null</code>
426          */
427         private String getTabEquivalent() {
428                 String tab;
429                 if (PHPeclipsePlugin.getDefault().getPreferenceStore().getBoolean(
430                                 PreferenceConstants.EDITOR_SPACES_FOR_TABS)) {
431                         int size = JavaCore.getPlugin().getPluginPreferences().getInt(
432                                         DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE);
433                         StringBuffer buf = new StringBuffer();
434                         for (int i = 0; i < size; i++)
435                                 buf.append(' ');
436                         tab = buf.toString();
437                 } else
438                         tab = "\t"; //$NON-NLS-1$
439
440                 return tab;
441         }
442
443         /**
444          * Returns the editor's selection provider.
445          * 
446          * @return the editor's selection provider or <code>null</code>
447          */
448         private ISelectionProvider getSelectionProvider() {
449                 ITextEditor editor = getTextEditor();
450                 if (editor != null) {
451                         return editor.getSelectionProvider();
452                 }
453                 return null;
454         }
455
456         /*
457          * @see org.eclipse.ui.texteditor.IUpdate#update()
458          */
459         public void update() {
460                 super.update();
461
462                 if (isEnabled())
463                         if (fIsTabAction)
464                                 setEnabled(canModifyEditor() && isSmartMode()
465                                                 && isValidSelection());
466                         else
467                                 setEnabled(canModifyEditor() && !getSelection().isEmpty());
468         }
469
470         /**
471          * Returns if the current selection is valid, i.e. whether it is empty and
472          * the caret in the whitespace at the start of a line, or covers multiple
473          * lines.
474          * 
475          * @return <code>true</code> if the selection is valid for an indent
476          *         operation
477          */
478         private boolean isValidSelection() {
479                 ITextSelection selection = getSelection();
480                 if (selection.isEmpty())
481                         return false;
482
483                 int offset = selection.getOffset();
484                 int length = selection.getLength();
485
486                 IDocument document = getDocument();
487                 if (document == null)
488                         return false;
489
490                 try {
491                         IRegion firstLine = document.getLineInformationOfOffset(offset);
492                         int lineOffset = firstLine.getOffset();
493
494                         // either the selection has to be empty and the caret in the WS at
495                         // the line start
496                         // or the selection has to extend over multiple lines
497                         if (length == 0)
498                                 return document.get(lineOffset, offset - lineOffset).trim()
499                                                 .length() == 0;
500                         else
501                                 // return lineOffset + firstLine.getLength() < offset + length;
502                                 return false; // only enable for empty selections for now
503
504                 } catch (BadLocationException e) {
505                 }
506
507                 return false;
508         }
509
510         /**
511          * Returns the smart preference state.
512          * 
513          * @return <code>true</code> if smart mode is on, <code>false</code>
514          *         otherwise
515          */
516         private boolean isSmartMode() {
517                 ITextEditor editor = getTextEditor();
518
519                 if (editor instanceof ITextEditorExtension3)
520                         return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;
521
522                 return false;
523         }
524
525         /**
526          * Returns the document currently displayed in the editor, or
527          * <code>null</code> if none can be obtained.
528          * 
529          * @return the current document or <code>null</code>
530          */
531         private IDocument getDocument() {
532
533                 ITextEditor editor = getTextEditor();
534                 if (editor != null) {
535
536                         IDocumentProvider provider = editor.getDocumentProvider();
537                         IEditorInput input = editor.getEditorInput();
538                         if (provider != null && input != null)
539                                 return provider.getDocument(input);
540
541                 }
542                 return null;
543         }
544
545         /**
546          * Returns the selection on the editor or an invalid selection if none can
547          * be obtained. Returns never <code>null</code>.
548          * 
549          * @return the current selection, never <code>null</code>
550          */
551         private ITextSelection getSelection() {
552                 ISelectionProvider provider = getSelectionProvider();
553                 if (provider != null) {
554
555                         ISelection selection = provider.getSelection();
556                         if (selection instanceof ITextSelection)
557                                 return (ITextSelection) selection;
558                 }
559
560                 // null object
561                 return TextSelection.emptySelection();
562         }
563
564 }