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