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;
30 //import org.eclipse.jface.text.Assert;
31 import org.eclipse.core.runtime.Assert;
32 import org.eclipse.jface.text.BadLocationException;
33 import org.eclipse.jface.text.DocumentCommand;
34 import org.eclipse.jface.text.IDocument;
35 import org.eclipse.jface.text.IRegion;
36 import org.eclipse.jface.text.IRewriteTarget;
37 import org.eclipse.jface.text.ITextSelection;
38 import org.eclipse.jface.text.ITypedRegion;
39 import org.eclipse.jface.text.Position;
40 import org.eclipse.jface.text.Region;
41 import org.eclipse.jface.text.TextSelection;
42 import org.eclipse.jface.text.TextUtilities;
43 import org.eclipse.jface.text.source.ISourceViewer;
44 import org.eclipse.jface.viewers.ISelection;
45 import org.eclipse.jface.viewers.ISelectionProvider;
46 import org.eclipse.swt.custom.BusyIndicator;
47 import org.eclipse.swt.widgets.Display;
48 import org.eclipse.text.edits.MalformedTreeException;
49 import org.eclipse.text.edits.ReplaceEdit;
50 import org.eclipse.text.edits.TextEdit;
51 import org.eclipse.ui.IEditorInput;
52 import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
53 import org.eclipse.ui.texteditor.IDocumentProvider;
54 import org.eclipse.ui.texteditor.ITextEditor;
55 import org.eclipse.ui.texteditor.ITextEditorExtension3;
56 import org.eclipse.ui.texteditor.TextEditorAction;
59 * Indents a line or range of lines in a Java document to its correct position.
60 * No complete AST must be present, the indentation is computed using
61 * heuristics. The algorith used is fast for single lines, but does not store
62 * any information and therefore not so efficient for large line ranges.
64 * @see net.sourceforge.phpdt.internal.ui.text.JavaHeuristicScanner
65 * @see net.sourceforge.phpdt.internal.ui.text.JavaIndenter
68 public class IndentAction extends TextEditorAction {
70 /** The caret offset after an indent operation. */
71 private int fCaretOffset;
74 * Whether this is the action invoked by TAB. When <code>true</code>,
75 * indentation behaves differently to accomodate normal TAB operation.
77 private final boolean fIsTabAction;
80 * Creates a new instance.
85 * the prefix to use for keys in <code>bundle</code>
89 * whether the action should insert tabs if over the indentation
91 public IndentAction (ResourceBundle bundle,
94 boolean isTabAction) {
95 super (bundle, prefix, editor);
97 fIsTabAction = isTabAction;
101 * @see org.eclipse.jface.action.Action#run()
104 // update has been called by the framework
105 if (!isEnabled() || !validateEditorInputState())
108 ITextSelection selection = getSelection();
109 final IDocument document = getDocument();
111 if (document != null) {
113 final int offset = selection.getOffset();
114 final int length = selection.getLength();
115 final Position end = new Position(offset + length);
116 final int firstLine, nLines;
120 document.addPosition(end);
121 firstLine = document.getLineOfOffset(offset);
122 // check for marginal (zero-length) lines
123 int minusOne = length == 0 ? 0 : 1;
124 nLines = document.getLineOfOffset(offset + length - minusOne)
126 } catch (BadLocationException e) {
127 // will only happen on concurrent modification
128 PHPeclipsePlugin.log(new Status(IStatus.ERROR, PHPeclipsePlugin
129 .getPluginId(), IStatus.OK, "", e)); //$NON-NLS-1$
133 Runnable runnable = new Runnable() {
135 IRewriteTarget target = (IRewriteTarget) getTextEditor()
136 .getAdapter(IRewriteTarget.class);
137 if (target != null) {
138 target.beginCompoundChange();
139 target.setRedraw(false);
143 JavaHeuristicScanner scanner = new JavaHeuristicScanner(
145 JavaIndenter indenter = new JavaIndenter(document,
147 boolean hasChanged = false;
148 for (int i = 0; i < nLines; i++) {
149 hasChanged |= indentLine(document, firstLine + i,
150 offset, indenter, scanner);
153 // update caret position: move to new position when
154 // indenting just one line
155 // keep selection when indenting multiple
156 int newOffset, newLength;
158 newOffset = fCaretOffset;
160 } else if (nLines > 1) {
162 newLength = end.getOffset() - offset;
164 newOffset = fCaretOffset;
168 // always reset the selection if anything was replaced
169 // but not when we had a singleline nontab invocation
171 && (hasChanged || newOffset != offset || newLength != length))
172 selectAndReveal(newOffset, newLength);
174 document.removePosition(end);
175 } catch (BadLocationException e) {
176 // will only happen on concurrent modification
177 PHPeclipsePlugin.log(new Status(IStatus.ERROR,
178 PHPeclipsePlugin.getPluginId(), IStatus.OK,
179 "ConcurrentModification in IndentAction", e)); //$NON-NLS-1$
183 if (target != null) {
184 target.endCompoundChange();
185 target.setRedraw(true);
192 Display display = getTextEditor().getEditorSite()
193 .getWorkbenchWindow().getShell().getDisplay();
194 BusyIndicator.showWhile(display, runnable);
202 * Selects the given range on the editor.
205 * the selection offset
207 * the selection range
209 private void selectAndReveal(int newOffset, int newLength) {
210 Assert.isTrue(newOffset >= 0);
211 Assert.isTrue(newLength >= 0);
212 ITextEditor editor = getTextEditor();
213 if (editor instanceof PHPEditor) {
214 ISourceViewer viewer = ((PHPEditor) editor).getViewer();
216 viewer.setSelectedRange(newOffset, newLength);
218 // this is too intrusive, but will never get called anyway
219 getTextEditor().selectAndReveal(newOffset, newLength);
224 * Indents a single line using the java heuristic scanner. Javadoc and
225 * multiline comments are indented as specified by the
226 * <code>JavaDocAutoIndentStrategy</code>.
231 * the line to be indented
237 * the heuristic scanner
238 * @return <code>true</code> if <code>document</code> was modified,
239 * <code>false</code> otherwise
240 * @throws BadLocationException
241 * if the document got changed concurrently
243 private boolean indentLine(IDocument document, int line, int caret,
244 JavaIndenter indenter, JavaHeuristicScanner scanner)
245 throws BadLocationException {
246 IRegion currentLine = document.getLineInformation(line);
247 int offset = currentLine.getOffset();
248 int wsStart = offset; // where we start searching for non-WS; after
249 // the "//" in single line comments
251 String indent = null;
252 if (offset < document.getLength()) {
253 ITypedRegion partition = TextUtilities.getPartition(document,
254 IPHPPartitions.PHP_PARTITIONING, offset, true);
255 String type = partition.getType();
256 if (type.equals(IPHPPartitions.PHP_PHPDOC_COMMENT)
257 || type.equals(IPHPPartitions.PHP_MULTILINE_COMMENT)) {
259 // TODO this is a hack
261 // new JavaDocAutoIndentStrategy().indentLineAtOffset(document,
268 IRegion previousLine = document
269 .getLineInformation(line - 1);
270 start = previousLine.getOffset() + previousLine.getLength();
273 DocumentCommand command = new DocumentCommand() {
275 command.text = "\n"; //$NON-NLS-1$
276 command.offset = start;
277 new JavaDocAutoIndentStrategy(IPHPPartitions.PHP_PARTITIONING)
278 .customizeDocumentCommand(document, command);
280 while (to < command.text.length()
281 && Character.isWhitespace(command.text.charAt(to)))
283 indent = command.text.substring(1, to);
286 // } else if (!fIsTabAction && partition.getOffset() == offset
287 // && type.equals(IPHPPartitions.PHP_SINGLELINE_COMMENT)) {
289 // // line comment starting at position 0 -> indent inside
291 // while (slashes < document.getLength() - 1
292 // && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
295 // wsStart = offset + slashes;
297 // StringBuffer computed = indenter.computeIndentation(offset);
298 // int tabSize = PHPeclipsePlugin
300 // .getPreferenceStore()
302 // AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
303 // while (slashes > 0 && computed.length() > 0) {
304 // char c = computed.charAt(0);
306 // if (slashes > tabSize)
307 // slashes -= tabSize;
310 // else if (c == ' ')
315 // computed.deleteCharAt(0);
318 // indent = document.get(offset, wsStart - offset) + computed;
323 // standard java indentation
324 if (indent == null) {
325 StringBuffer computed = indenter.computeIndentation(offset);
326 if (computed != null)
327 indent = computed.toString();
329 //indent = new String();
330 return true; // prevent affecting html part
334 // get current white space
335 int lineLength = currentLine.getLength();
336 int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart,
337 offset + lineLength);
338 if (end == JavaHeuristicScanner.NOT_FOUND)
339 end = offset + lineLength;
340 int length = end - offset;
341 String currentIndent = document.get(offset, length);
343 // if we are right before the text start / line end, and already after
344 // the insertion point
345 // then just insert a tab.
346 if (fIsTabAction && caret == end
347 && whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
348 String tab = getTabEquivalent();
349 document.replace(caret, 0, tab);
350 fCaretOffset = caret + tab.length();
354 // set the caret offset so it can be used when setting the selection
355 if (caret >= offset && caret <= end)
356 fCaretOffset = offset + indent.length();
360 // only change the document if it is a real change
361 if (!indent.equals(currentIndent)) {
362 String deletedText = document.get(offset, length);
363 document.replace(offset, length, indent);
366 && indent.length() > currentIndent.length()
367 && PHPeclipsePlugin.getDefault().getPreferenceStore()
369 PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
370 ITextEditor editor = getTextEditor();
371 if (editor != null) {
372 final SmartBackspaceManager manager = (SmartBackspaceManager) editor
373 .getAdapter(SmartBackspaceManager.class);
374 if (manager != null) {
376 // restore smart portion
377 ReplaceEdit smart = new ReplaceEdit(offset, indent
378 .length(), deletedText);
380 final UndoSpec spec = new UndoSpec(offset
381 + indent.length(), new Region(caret, 0),
382 new TextEdit[] { smart }, 2, null);
383 manager.register(spec);
384 } catch (MalformedTreeException e) {
386 PHPeclipsePlugin.log(new Status(IStatus.ERROR,
387 PHPeclipsePlugin.getPluginId(), IStatus.OK,
388 "Illegal smart backspace action", e)); //$NON-NLS-1$
400 * Returns the size in characters of a string. All characters count one,
401 * tabs count the editor's preference for the tab display
404 * the string to be measured.
407 private int whiteSpaceLength(String indent) {
412 int l = indent.length();
413 int tabSize = PHPeclipsePlugin.getDefault().getPreferenceStore().getInt(
414 AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
416 for (int i = 0; i < l; i++)
417 size += indent.charAt(i) == '\t' ? tabSize : 1;
423 * Returns a tab equivalent, either as a tab character or as spaces,
424 * depending on the editor and formatter preferences.
426 * @return a string representing one tab in the editor, never
429 private String getTabEquivalent() {
431 if (PHPeclipsePlugin.getDefault().getPreferenceStore().getBoolean(
432 PreferenceConstants.EDITOR_SPACES_FOR_TABS)) {
433 int size = JavaCore.getPlugin().getPluginPreferences().getInt(
434 DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE);
435 StringBuffer buf = new StringBuffer();
436 for (int i = 0; i < size; i++)
438 tab = buf.toString();
440 tab = "\t"; //$NON-NLS-1$
446 * Returns the editor's selection provider.
448 * @return the editor's selection provider or <code>null</code>
450 private ISelectionProvider getSelectionProvider() {
451 ITextEditor editor = getTextEditor();
452 if (editor != null) {
453 return editor.getSelectionProvider();
459 * @see org.eclipse.ui.texteditor.IUpdate#update()
461 public void update() {
466 setEnabled(canModifyEditor() && isSmartMode()
467 && isValidSelection());
469 setEnabled(canModifyEditor() && !getSelection().isEmpty());
473 * Returns if the current selection is valid, i.e. whether it is empty and
474 * the caret in the whitespace at the start of a line, or covers multiple
477 * @return <code>true</code> if the selection is valid for an indent
480 private boolean isValidSelection() {
481 ITextSelection selection = getSelection();
483 if (selection.isEmpty()) {
487 int offset = selection.getOffset();
488 int length = selection.getLength();
490 IDocument document = getDocument();
492 if (document == null) {
497 IRegion firstLine = document.getLineInformationOfOffset(offset);
498 int lineOffset = firstLine.getOffset();
500 // either the selection has to be empty and the caret in the WS at
502 // or the selection has to extend over multiple lines
506 bRet = document.get (lineOffset, offset - lineOffset).trim().length() == 0;
511 // return lineOffset + firstLine.getLength() < offset + length;
512 return false; // only enable for empty selections for now
515 } catch (BadLocationException e) {
522 * Returns the smart preference state.
524 * @return <code>true</code> if smart mode is on, <code>false</code>
527 private boolean isSmartMode() {
528 ITextEditor editor = getTextEditor();
530 if (editor instanceof ITextEditorExtension3)
531 return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;
537 * Returns the document currently displayed in the editor, or
538 * <code>null</code> if none can be obtained.
540 * @return the current document or <code>null</code>
542 private IDocument getDocument() { ITextEditor editor = getTextEditor();
543 if (editor != null) {
544 IDocumentProvider provider = editor.getDocumentProvider();
545 IEditorInput input = editor.getEditorInput();
547 if (provider != null && input != null) {
548 return provider.getDocument(input);
556 * Returns the selection on the editor or an invalid selection if none can
557 * be obtained. Returns never <code>null</code>.
559 * @return the current selection, never <code>null</code>
561 private ITextSelection getSelection() {
562 ISelectionProvider provider = getSelectionProvider();
563 if (provider != null) {
565 ISelection selection = provider.getSelection();
566 if (selection instanceof ITextSelection)
567 return (ITextSelection) selection;
571 return TextSelection.emptySelection();