Refactory: nl1 plugin.
[phpeclipse.git] / net.sourceforge.phpeclipse.ui / 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 import net.sourceforge.phpeclipse.ui.WebUI;
27
28 import org.eclipse.core.runtime.IStatus;
29 import org.eclipse.core.runtime.Status;
30 import org.eclipse.jface.text.Assert;
31 import org.eclipse.jface.text.BadLocationException;
32 import org.eclipse.jface.text.DocumentCommand;
33 import org.eclipse.jface.text.IDocument;
34 import org.eclipse.jface.text.IRegion;
35 import org.eclipse.jface.text.IRewriteTarget;
36 import org.eclipse.jface.text.ITextSelection;
37 import org.eclipse.jface.text.ITypedRegion;
38 import org.eclipse.jface.text.Position;
39 import org.eclipse.jface.text.Region;
40 import org.eclipse.jface.text.TextSelection;
41 import org.eclipse.jface.text.TextUtilities;
42 import org.eclipse.jface.text.source.ISourceViewer;
43 import org.eclipse.jface.viewers.ISelection;
44 import org.eclipse.jface.viewers.ISelectionProvider;
45 import org.eclipse.swt.custom.BusyIndicator;
46 import org.eclipse.swt.widgets.Display;
47 import org.eclipse.text.edits.MalformedTreeException;
48 import org.eclipse.text.edits.ReplaceEdit;
49 import org.eclipse.text.edits.TextEdit;
50 import org.eclipse.ui.IEditorInput;
51 import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
52 import org.eclipse.ui.texteditor.IDocumentProvider;
53 import org.eclipse.ui.texteditor.ITextEditor;
54 import org.eclipse.ui.texteditor.ITextEditorExtension3;
55 import org.eclipse.ui.texteditor.TextEditorAction;
56
57 /**
58  * Indents a line or range of lines in a Java document to its correct position.
59  * No complete AST must be present, the indentation is computed using
60  * heuristics. The algorith used is fast for single lines, but does not store
61  * any information and therefore not so efficient for large line 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>,
74          * indentation behaves differently to accomodate normal TAB operation.
75          */
76         private final boolean fIsTabAction;
77
78         /**
79          * Creates a new instance.
80          * 
81          * @param bundle
82          *            the resource bundle
83          * @param prefix
84          *            the prefix to use for keys in <code>bundle</code>
85          * @param editor
86          *            the text editor
87          * @param isTabAction
88          *            whether the action should insert tabs if over the indentation
89          */
90         public IndentAction(ResourceBundle bundle, String prefix,
91                         ITextEditor editor, boolean isTabAction) {
92                 super(bundle, prefix, editor);
93                 fIsTabAction = isTabAction;
94         }
95
96         /*
97          * @see org.eclipse.jface.action.Action#run()
98          */
99         public void run() {
100                 // update has been called by the framework
101                 if (!isEnabled() || !validateEditorInputState())
102                         return;
103
104                 ITextSelection selection = getSelection();
105                 final IDocument document = getDocument();
106
107                 if (document != null) {
108
109                         final int offset = selection.getOffset();
110                         final int length = selection.getLength();
111                         final Position end = new Position(offset + length);
112                         final int firstLine, nLines;
113                         fCaretOffset = -1;
114
115                         try {
116                                 document.addPosition(end);
117                                 firstLine = document.getLineOfOffset(offset);
118                                 // check for marginal (zero-length) lines
119                                 int minusOne = length == 0 ? 0 : 1;
120                                 nLines = document.getLineOfOffset(offset + length - minusOne)
121                                                 - firstLine + 1;
122                         } catch (BadLocationException e) {
123                                 // will only happen on concurrent modification
124                                 WebUI.log(new Status(IStatus.ERROR, WebUI
125                                                 .getPluginId(), IStatus.OK, "", e)); //$NON-NLS-1$
126                                 return;
127                         }
128
129                         Runnable runnable = new Runnable() {
130                                 public void run() {
131                                         IRewriteTarget target = (IRewriteTarget) getTextEditor()
132                                                         .getAdapter(IRewriteTarget.class);
133                                         if (target != null) {
134                                                 target.beginCompoundChange();
135                                                 target.setRedraw(false);
136                                         }
137
138                                         try {
139                                                 JavaHeuristicScanner scanner = new JavaHeuristicScanner(
140                                                                 document);
141                                                 JavaIndenter indenter = new JavaIndenter(document,
142                                                                 scanner);
143                                                 boolean hasChanged = false;
144                                                 for (int i = 0; i < nLines; i++) {
145                                                         hasChanged |= indentLine(document, firstLine + i,
146                                                                         offset, indenter, scanner);
147                                                 }
148
149                                                 // update caret position: move to new position when
150                                                 // indenting just one line
151                                                 // keep selection when indenting multiple
152                                                 int newOffset, newLength;
153                                                 if (fIsTabAction) {
154                                                         newOffset = fCaretOffset;
155                                                         newLength = 0;
156                                                 } else if (nLines > 1) {
157                                                         newOffset = offset;
158                                                         newLength = end.getOffset() - offset;
159                                                 } else {
160                                                         newOffset = fCaretOffset;
161                                                         newLength = 0;
162                                                 }
163
164                                                 // always reset the selection if anything was replaced
165                                                 // but not when we had a singleline nontab invocation
166                                                 if (newOffset != -1
167                                                                 && (hasChanged || newOffset != offset || newLength != length))
168                                                         selectAndReveal(newOffset, newLength);
169
170                                                 document.removePosition(end);
171                                         } catch (BadLocationException e) {
172                                                 // will only happen on concurrent modification
173                                                 WebUI.log(new Status(IStatus.ERROR,
174                                                                 WebUI.getPluginId(), IStatus.OK,
175                                                                 "ConcurrentModification in IndentAction", e)); //$NON-NLS-1$
176
177                                         } finally {
178
179                                                 if (target != null) {
180                                                         target.endCompoundChange();
181                                                         target.setRedraw(true);
182                                                 }
183                                         }
184                                 }
185                         };
186
187                         if (nLines > 50) {
188                                 Display display = getTextEditor().getEditorSite()
189                                                 .getWorkbenchWindow().getShell().getDisplay();
190                                 BusyIndicator.showWhile(display, runnable);
191                         } else
192                                 runnable.run();
193
194                 }
195         }
196
197         /**
198          * Selects the given range on the editor.
199          * 
200          * @param newOffset
201          *            the selection offset
202          * @param newLength
203          *            the selection range
204          */
205         private void selectAndReveal(int newOffset, int newLength) {
206                 Assert.isTrue(newOffset >= 0);
207                 Assert.isTrue(newLength >= 0);
208                 ITextEditor editor = getTextEditor();
209                 if (editor instanceof PHPEditor) {
210                         ISourceViewer viewer = ((PHPEditor) editor).getViewer();
211                         if (viewer != null)
212                                 viewer.setSelectedRange(newOffset, newLength);
213                 } else
214                         // this is too intrusive, but will never get called anyway
215                         getTextEditor().selectAndReveal(newOffset, newLength);
216
217         }
218
219         /**
220          * Indents a single line using the java heuristic scanner. Javadoc and
221          * multiline comments are indented as specified by the
222          * <code>JavaDocAutoIndentStrategy</code>.
223          * 
224          * @param document
225          *            the document
226          * @param line
227          *            the line to be indented
228          * @param caret
229          *            the caret position
230          * @param indenter
231          *            the java indenter
232          * @param scanner
233          *            the heuristic scanner
234          * @return <code>true</code> if <code>document</code> was modified,
235          *         <code>false</code> otherwise
236          * @throws BadLocationException
237          *             if the document got changed concurrently
238          */
239         private boolean indentLine(IDocument document, int line, int caret,
240                         JavaIndenter indenter, JavaHeuristicScanner scanner)
241                         throws BadLocationException {
242                 IRegion currentLine = document.getLineInformation(line);
243                 int offset = currentLine.getOffset();
244                 int wsStart = offset; // where we start searching for non-WS; after
245                                                                 // the "//" in single line comments
246
247                 String indent = null;
248                 if (offset < document.getLength()) {
249                         ITypedRegion partition = TextUtilities.getPartition(document,
250                                         IPHPPartitions.PHP_PARTITIONING, offset, true);
251                         String type = partition.getType();
252                         if (type.equals(IPHPPartitions.PHP_PHPDOC_COMMENT)
253                                         || type.equals(IPHPPartitions.PHP_MULTILINE_COMMENT)) {
254
255                                 // TODO this is a hack
256                                 // what I want to do
257                                 // new JavaDocAutoIndentStrategy().indentLineAtOffset(document,
258                                 // offset);
259                                 // return;
260
261                                 int start = 0;
262                                 if (line > 0) {
263
264                                         IRegion previousLine = document
265                                                         .getLineInformation(line - 1);
266                                         start = previousLine.getOffset() + previousLine.getLength();
267                                 }
268
269                                 DocumentCommand command = new DocumentCommand() {
270                                 };
271                                 command.text = "\n"; //$NON-NLS-1$
272                                 command.offset = start;
273                                 new JavaDocAutoIndentStrategy(IPHPPartitions.PHP_PARTITIONING)
274                                                 .customizeDocumentCommand(document, command);
275                                 int to = 1;
276                                 while (to < command.text.length()
277                                                 && Character.isWhitespace(command.text.charAt(to)))
278                                         to++;
279                                 indent = command.text.substring(1, to);
280
281 // omit Java style
282 //                      } else if (!fIsTabAction && partition.getOffset() == offset
283 //                                      && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
284 //
285 //                              // line comment starting at position 0 -> indent inside
286 //                              int slashes = 2;
287 //                              while (slashes < document.getLength() - 1
288 //                                              && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
289 //                                      slashes += 2;
290 //
291 //                              wsStart = offset + slashes;
292 //
293 //                              StringBuffer computed = indenter.computeIndentation(offset);
294 //                              int tabSize = PHPeclipsePlugin
295 //                                              .getDefault()
296 //                                              .getPreferenceStore()
297 //                                              .getInt(
298 //                                                              AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
299 //                              while (slashes > 0 && computed.length() > 0) {
300 //                                      char c = computed.charAt(0);
301 //                                      if (c == '\t')
302 //                                              if (slashes > tabSize)
303 //                                                      slashes -= tabSize;
304 //                                              else
305 //                                                      break;
306 //                                      else if (c == ' ')
307 //                                              slashes--;
308 //                                      else
309 //                                              break;
310 //
311 //                                      computed.deleteCharAt(0);
312 //                              }
313 //
314 //                              indent = document.get(offset, wsStart - offset) + computed;
315
316                         }
317                 }
318
319                 // standard java indentation
320                 if (indent == null) {
321                         StringBuffer computed = indenter.computeIndentation(offset);
322                         if (computed != null)
323                                 indent = computed.toString();
324                         else
325                                 //indent = new String();
326                                 return true; // prevent affecting html part
327                 }
328
329                 // change document:
330                 // get current white space
331                 int lineLength = currentLine.getLength();
332                 int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart,
333                                 offset + lineLength);
334                 if (end == JavaHeuristicScanner.NOT_FOUND)
335                         end = offset + lineLength;
336                 int length = end - offset;
337                 String currentIndent = document.get(offset, length);
338
339                 // if we are right before the text start / line end, and already after
340                 // the insertion point
341                 // then just insert a tab.
342                 if (fIsTabAction && caret == end
343                                 && whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
344                         String tab = getTabEquivalent();
345                         document.replace(caret, 0, tab);
346                         fCaretOffset = caret + tab.length();
347                         return true;
348                 }
349
350                 // set the caret offset so it can be used when setting the selection
351                 if (caret >= offset && caret <= end)
352                         fCaretOffset = offset + indent.length();
353                 else
354                         fCaretOffset = -1;
355
356                 // only change the document if it is a real change
357                 if (!indent.equals(currentIndent)) {
358                         String deletedText = document.get(offset, length);
359                         document.replace(offset, length, indent);
360
361                         if (fIsTabAction
362                                         && indent.length() > currentIndent.length()
363                                         && WebUI.getDefault().getPreferenceStore()
364                                                         .getBoolean(
365                                                                         PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
366                                 ITextEditor editor = getTextEditor();
367                                 if (editor != null) {
368                                         final SmartBackspaceManager manager = (SmartBackspaceManager) editor
369                                                         .getAdapter(SmartBackspaceManager.class);
370                                         if (manager != null) {
371                                                 try {
372                                                         // restore smart portion
373                                                         ReplaceEdit smart = new ReplaceEdit(offset, indent
374                                                                         .length(), deletedText);
375
376                                                         final UndoSpec spec = new UndoSpec(offset
377                                                                         + indent.length(), new Region(caret, 0),
378                                                                         new TextEdit[] { smart }, 2, null);
379                                                         manager.register(spec);
380                                                 } catch (MalformedTreeException e) {
381                                                         // log & ignore
382                                                         WebUI.log(new Status(IStatus.ERROR,
383                                                                         WebUI.getPluginId(), IStatus.OK,
384                                                                         "Illegal smart backspace action", e)); //$NON-NLS-1$
385                                                 }
386                                         }
387                                 }
388                         }
389
390                         return true;
391                 } else
392                         return false;
393         }
394
395         /**
396          * Returns the size in characters of a string. All characters count one,
397          * tabs count the editor's preference for the tab display
398          * 
399          * @param indent
400          *            the string to be measured.
401          * @return
402          */
403         private int whiteSpaceLength(String indent) {
404                 if (indent == null)
405                         return 0;
406                 else {
407                         int size = 0;
408                         int l = indent.length();
409                         int tabSize = WebUI.getDefault().getPreferenceStore().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 (WebUI.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 }