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