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