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
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.actions;
13 import java.util.ResourceBundle;
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;
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;
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
63 * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner
64 * @see net.sourceforge.phpdt.internal.ui.text.JavaIndenter
67 public class IndentAction extends TextEditorAction {
69 /** The caret offset after an indent operation. */
70 private int fCaretOffset;
73 * Whether this is the action invoked by TAB. When <code>true</code>, indentation behaves
74 * differently to accomodate normal TAB operation.
76 private final boolean fIsTabAction;
79 * Creates a new instance.
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
86 public IndentAction(ResourceBundle bundle, String prefix, ITextEditor editor, boolean isTabAction) {
87 super(bundle, prefix, editor);
88 fIsTabAction= isTabAction;
92 * @see org.eclipse.jface.action.Action#run()
95 // update has been called by the framework
96 if (!isEnabled() || !validateEditorInputState())
99 ITextSelection selection= getSelection();
100 final IDocument document= getDocument();
102 if (document != null) {
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;
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$
122 Runnable runnable= new Runnable() {
124 IRewriteTarget target= (IRewriteTarget)getTextEditor().getAdapter(IRewriteTarget.class);
125 if (target != null) {
126 target.beginCompoundChange();
127 target.setRedraw(false);
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);
138 // update caret position: move to new position when indenting just one line
139 // keep selection when indenting multiple
140 int newOffset, newLength;
142 newOffset= fCaretOffset;
144 } else if (nLines > 1) {
146 newLength= end.getOffset() - offset;
148 newOffset= fCaretOffset;
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);
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$
164 if (target != null) {
165 target.endCompoundChange();
166 target.setRedraw(true);
173 Display display= getTextEditor().getEditorSite().getWorkbenchWindow().getShell().getDisplay();
174 BusyIndicator.showWhile(display, runnable);
182 * Selects the given range on the editor.
184 * @param newOffset the selection offset
185 * @param newLength the selection range
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();
194 viewer.setSelectedRange(newOffset, newLength);
196 // this is too intrusive, but will never get called anyway
197 getTextEditor().selectAndReveal(newOffset, newLength);
202 * Indents a single line using the java heuristic scanner. Javadoc and multiline comments are
203 * indented as specified by the <code>JavaDocAutoIndentStrategy</code>.
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
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
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)) {
224 // TODO this is a hack
226 // new JavaDocAutoIndentStrategy().indentLineAtOffset(document, offset);
232 IRegion previousLine= document.getLineInformation(line - 1);
233 start= previousLine.getOffset() + previousLine.getLength();
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);
241 while (to < command.text.length() && Character.isWhitespace(command.text.charAt(to)))
243 indent= command.text.substring(1, to);
245 } else if (!fIsTabAction && partition.getOffset() == offset && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
247 // line comment starting at position 0 -> indent inside
249 while (slashes < document.getLength() - 1 && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
252 wsStart= offset + slashes;
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);
259 if (slashes > tabSize)
267 computed.deleteCharAt(0);
270 indent= document.get(offset, wsStart - offset) + computed;
275 // standard java indentation
276 if (indent == null) {
277 StringBuffer computed= indenter.computeIndentation(offset);
278 if (computed != null)
279 indent= computed.toString();
281 indent= new String();
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);
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();
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();
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);
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) {
319 // restore smart portion
320 ReplaceEdit smart= new ReplaceEdit(offset, indent.length(), deletedText);
322 final UndoSpec spec= new UndoSpec(
323 offset + indent.length(),
324 new Region(caret, 0),
325 new TextEdit[] { smart },
328 manager.register(spec);
329 } catch (MalformedTreeException e) {
331 PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin.getPluginId(), IStatus.OK, "Illegal smart backspace action", e)); //$NON-NLS-1$
344 * Returns the size in characters of a string. All characters count one, tabs count the editor's
345 * preference for the tab display
347 * @param indent the string to be measured.
350 private int whiteSpaceLength(String indent) {
355 int l= indent.length();
356 int tabSize= PHPeclipsePlugin.getDefault().getPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
358 for (int i= 0; i < l; i++)
359 size += indent.charAt(i) == '\t' ? tabSize : 1;
365 * Returns a tab equivalent, either as a tab character or as spaces, depending on the editor and
366 * formatter preferences.
368 * @return a string representing one tab in the editor, never <code>null</code>
370 private String getTabEquivalent() {
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++)
379 tab= "\t"; //$NON-NLS-1$
385 * Returns the editor's selection provider.
387 * @return the editor's selection provider or <code>null</code>
389 private ISelectionProvider getSelectionProvider() {
390 ITextEditor editor= getTextEditor();
391 if (editor != null) {
392 return editor.getSelectionProvider();
398 * @see org.eclipse.ui.texteditor.IUpdate#update()
400 public void update() {
405 setEnabled(canModifyEditor() && isSmartMode() && isValidSelection());
407 setEnabled(canModifyEditor() && !getSelection().isEmpty());
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.
414 * @return <code>true</code> if the selection is valid for an indent operation
416 private boolean isValidSelection() {
417 ITextSelection selection= getSelection();
418 if (selection.isEmpty())
421 int offset= selection.getOffset();
422 int length= selection.getLength();
424 IDocument document= getDocument();
425 if (document == null)
429 IRegion firstLine= document.getLineInformationOfOffset(offset);
430 int lineOffset= firstLine.getOffset();
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
435 return document.get(lineOffset, offset - lineOffset).trim().length() == 0;
437 // return lineOffset + firstLine.getLength() < offset + length;
438 return false; // only enable for empty selections for now
440 } catch (BadLocationException e) {
447 * Returns the smart preference state.
449 * @return <code>true</code> if smart mode is on, <code>false</code> otherwise
451 private boolean isSmartMode() {
452 ITextEditor editor= getTextEditor();
454 if (editor instanceof ITextEditorExtension3)
455 return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;
461 * Returns the document currently displayed in the editor, or <code>null</code> if none can be
464 * @return the current document or <code>null</code>
466 private IDocument getDocument() {
468 ITextEditor editor= getTextEditor();
469 if (editor != null) {
471 IDocumentProvider provider= editor.getDocumentProvider();
472 IEditorInput input= editor.getEditorInput();
473 if (provider != null && input != null)
474 return provider.getDocument(input);
481 * Returns the selection on the editor or an invalid selection if none can be obtained. Returns
482 * never <code>null</code>.
484 * @return the current selection, never <code>null</code>
486 private ITextSelection getSelection() {
487 ISelectionProvider provider= getSelectionProvider();
488 if (provider != null) {
490 ISelection selection= provider.getSelection();
491 if (selection instanceof ITextSelection)
492 return (ITextSelection) selection;
496 return TextSelection.emptySelection();