| /******************************************************************************* |
| * Copyright (c) 2000, 2010, 2013 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Sergey Prigogin, Google |
| * Anton Leherbauer (Wind River Systems) |
| * Red Hat Inc. - modified for use in SystemTap |
| *******************************************************************************/ |
| package org.eclipse.linuxtools.internal.systemtap.ui.ide.handlers; |
| |
| import org.eclipse.core.commands.AbstractHandler; |
| import org.eclipse.core.commands.ExecutionEvent; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.IRewriteTarget; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.text.ITypedRegion; |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.TextSelection; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jface.text.source.ISourceViewer; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionProvider; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.IDEPlugin; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.IndentUtil; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPDefaultCodeFormatterConstants; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPEditor; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPHeuristicScanner; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPIndenter; |
| import org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPPartitionScanner; |
| import org.eclipse.swt.custom.BusyIndicator; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.IEditorInput; |
| import org.eclipse.ui.IFileEditorInput; |
| import org.eclipse.ui.handlers.HandlerUtil; |
| import org.eclipse.ui.texteditor.IDocumentProvider; |
| import org.eclipse.ui.texteditor.ITextEditor; |
| |
| /** |
| * Indents a line or range of lines in a C document to its correct position. No |
| * complete AST must be present, the indentation is computed using heuristics. |
| * The algorithm used is fast for single lines, but does not store any |
| * information and therefore not so efficient for large line ranges. |
| * |
| * @see org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPHeuristicScanner |
| * @see org.eclipse.linuxtools.internal.systemtap.ui.ide.editors.stp.STPIndenter |
| */ |
| public class IndentHandler extends AbstractHandler { |
| /** The caret offset after an indent operation. */ |
| private int fCaretOffset; |
| |
| private class STPRunnable implements Runnable { |
| private ITextEditor editor; |
| |
| public STPRunnable(ITextEditor editor) { |
| this.editor = editor; |
| } |
| |
| public ITextEditor getTextEditor() { |
| return editor; |
| } |
| |
| @Override |
| public void run() { |
| } |
| } |
| |
| /** |
| * Whether this is the action invoked by TAB. When <code>true</code>, |
| * indentation behaves differently to accommodate normal TAB operation. |
| */ |
| private final boolean fIsTabAction = false; |
| |
| @Override |
| public Object execute(ExecutionEvent event) { // Update has been called by |
| // the framework |
| if (!isEnabled()) |
| return null; |
| |
| ITextEditor editor = (ITextEditor) HandlerUtil.getActiveEditor(event); |
| if (editor == null || !editor.isEditable()) { |
| return null; |
| } |
| |
| ITextSelection selection = getSelection(editor); |
| final IDocument document = getDocument(editor); |
| |
| if (document != null) { |
| final int offset = selection.getOffset(); |
| final int length = selection.getLength(); |
| final Position end = new Position(offset + length); |
| final int firstLine, nLines; |
| fCaretOffset = -1; |
| |
| try { |
| firstLine = document.getLineOfOffset(offset); |
| // check for marginal (zero-length) lines |
| int minusOne = length == 0 ? 0 : 1; |
| nLines = document.getLineOfOffset(offset + length - minusOne) |
| - firstLine + 1; |
| document.addPosition(end); |
| } catch (BadLocationException e) { |
| // will only happen on concurrent modification |
| IDEPlugin.log(new Status(IStatus.ERROR, |
| IDEPlugin.PLUGIN_ID, IStatus.OK, "", e)); //$NON-NLS-1$ |
| return null; |
| } |
| |
| Runnable runnable = new STPRunnable(editor) { |
| @Override |
| public void run() { |
| IRewriteTarget target = getTextEditor() |
| .getAdapter(IRewriteTarget.class); |
| if (target != null) { |
| target.beginCompoundChange(); |
| } |
| |
| try { |
| STPHeuristicScanner scanner = new STPHeuristicScanner( |
| document); |
| STPIndenter indenter = new STPIndenter(document, |
| scanner, getProject(getTextEditor())); |
| final boolean multiLine = nLines > 1; |
| boolean hasChanged = false; |
| for (int i = 0; i < nLines; i++) { |
| hasChanged |= indentLine(document, firstLine + i, |
| offset, indenter, scanner, multiLine); |
| } |
| |
| // update caret position: move to new position when |
| // indenting just one line |
| // keep selection when indenting multiple |
| int newOffset, newLength; |
| if (!fIsTabAction && multiLine) { |
| newOffset = offset; |
| newLength = end.getOffset() - offset; |
| } else { |
| newOffset = fCaretOffset; |
| newLength = 0; |
| } |
| |
| // always reset the selection if anything was replaced |
| // but not when we had a single line non-tab invocation |
| if (newOffset != -1 |
| && (hasChanged || newOffset != offset || newLength != length)) |
| selectAndReveal(getTextEditor(), newOffset, |
| newLength); |
| |
| } catch (BadLocationException e) { |
| // will only happen on concurrent modification |
| IDEPlugin.log(new Status(IStatus.ERROR, IDEPlugin |
| .PLUGIN_ID, IStatus.OK, |
| "ConcurrentModification in IndentAction", e)); //$NON-NLS-1$ |
| } finally { |
| document.removePosition(end); |
| if (target != null) { |
| target.endCompoundChange(); |
| } |
| } |
| } |
| }; |
| |
| if (nLines > 50) { |
| Display display = editor.getEditorSite().getWorkbenchWindow() |
| .getShell().getDisplay(); |
| BusyIndicator.showWhile(display, runnable); |
| } else { |
| runnable.run(); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Selects the given range on the editor. |
| * |
| * @param newOffset |
| * the selection offset |
| * @param newLength |
| * the selection range |
| */ |
| private void selectAndReveal(ITextEditor editor, int newOffset, |
| int newLength) { |
| Assert.isTrue(newOffset >= 0); |
| Assert.isTrue(newLength >= 0); |
| if (editor instanceof STPEditor) { |
| ISourceViewer viewer = ((STPEditor) editor).getMySourceViewer(); |
| if (viewer != null) { |
| viewer.setSelectedRange(newOffset, newLength); |
| } |
| } else { |
| // this is too intrusive, but will never get called anyway |
| editor.selectAndReveal(newOffset, newLength); |
| } |
| } |
| |
| /** |
| * Indents a single line using the heuristic scanner. Multiline comments are |
| * indented as specified by the <code>CCommentAutoIndentStrategy</code>. |
| * |
| * @param document |
| * the document |
| * @param line |
| * the line to be indented |
| * @param caret |
| * the caret position |
| * @param indenter |
| * the indenter |
| * @param scanner |
| * the heuristic scanner |
| * @param multiLine |
| * <code>true</code> if more than one line is being indented |
| * @return <code>true</code> if <code>document</code> was modified, |
| * <code>false</code> otherwise |
| * @throws BadLocationException |
| * if the document got changed concurrently |
| */ |
| private boolean indentLine(IDocument document, |
| int line, int caret, STPIndenter indenter, |
| STPHeuristicScanner scanner, boolean multiLine) |
| throws BadLocationException { |
| IRegion currentLine = document.getLineInformation(line); |
| int offset = currentLine.getOffset(); |
| int wsStart = offset; // where we start searching for non-WS; after the |
| // "//" in single line comments |
| |
| String indent = null; |
| if (offset < document.getLength()) { |
| ITypedRegion partition = TextUtilities.getPartition(document, |
| STPPartitionScanner.STP_PARTITIONING, offset, true); |
| ITypedRegion startingPartition = TextUtilities.getPartition( |
| document, STPPartitionScanner.STP_PARTITIONING, offset, |
| false); |
| String type = partition.getType(); |
| if (type.equals(STPPartitionScanner.STP_MULTILINE_COMMENT)) { |
| indent = computeCommentIndent(document, line, scanner, |
| startingPartition); |
| } else if (startingPartition.getType().equals( |
| STPPartitionScanner.STP_CONDITIONAL)) { |
| indent = computePreprocessorIndent(document, line, |
| startingPartition); |
| } else if (startingPartition.getType().equals( |
| STPPartitionScanner.STP_STRING) |
| && offset > startingPartition.getOffset()) { |
| // don't indent inside (raw-)string |
| return false; |
| } else if (!fIsTabAction |
| && startingPartition.getOffset() == offset |
| && startingPartition.getType().equals( |
| STPPartitionScanner.STP_COMMENT)) { |
| // line comment starting at position 0 -> indent inside |
| if (indentInsideLineComments()) { |
| int max = document.getLength() - offset; |
| int slashes = 2; |
| while (slashes < max - 1 |
| && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$ |
| slashes += 2; |
| |
| wsStart = offset + slashes; |
| |
| StringBuilder computed = indenter |
| .computeIndentation(offset); |
| if (computed == null) |
| computed = new StringBuilder(0); |
| int tabSize = getTabSize(); |
| while (slashes > 0 && computed.length() > 0) { |
| char c = computed.charAt(0); |
| if (c == '\t') { |
| if (slashes > tabSize) { |
| slashes -= tabSize; |
| } else { |
| break; |
| } |
| } else if (c == ' ') { |
| slashes--; |
| } else { |
| break; |
| } |
| |
| computed.deleteCharAt(0); |
| } |
| |
| indent = document.get(offset, wsStart - offset) + computed; |
| } |
| } |
| } |
| |
| // standard C code indentation |
| if (indent == null) { |
| StringBuilder computed = indenter.computeIndentation(offset); |
| if (computed != null) { |
| indent = computed.toString(); |
| } else { |
| indent = ""; //$NON-NLS-1$ |
| } |
| } |
| |
| // change document: |
| // get current white space |
| int lineLength = currentLine.getLength(); |
| int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart, |
| offset + lineLength); |
| if (end == STPHeuristicScanner.NOT_FOUND) { |
| // an empty line |
| end = offset + lineLength; |
| if (multiLine && !indentEmptyLines()) { |
| indent = ""; //$NON-NLS-1$ |
| } |
| } |
| int length = end - offset; |
| String currentIndent = document.get(offset, length); |
| |
| // set the caret offset so it can be used when setting the selection |
| if (caret >= offset && caret <= end) { |
| fCaretOffset = offset + indent.length(); |
| } else { |
| fCaretOffset = -1; |
| } |
| |
| // only change the document if it is a real change |
| if (!indent.equals(currentIndent)) { |
| document.replace(offset, length, indent); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Computes and returns the indentation for a block comment line. |
| * |
| * @param document |
| * the document |
| * @param line |
| * the line in document |
| * @param scanner |
| * the scanner |
| * @param partition |
| * the comment partition |
| * @return the indent, or <code>null</code> if not computable |
| * @throws BadLocationException |
| */ |
| private String computeCommentIndent(IDocument document, int line, |
| STPHeuristicScanner scanner, ITypedRegion partition) |
| throws BadLocationException { |
| return IndentUtil.computeCommentIndent(document, line, scanner, |
| partition); |
| } |
| |
| /** |
| * Computes and returns the indentation for a preprocessor line. |
| * |
| * @param document |
| * the document |
| * @param line |
| * the line in document |
| * @param partition |
| * the comment partition |
| * @return the indent, or <code>null</code> if not computable |
| * @throws BadLocationException |
| */ |
| private String computePreprocessorIndent(IDocument document, int line, |
| ITypedRegion partition) throws BadLocationException { |
| return IndentUtil.computePreprocessorIndent(document, line, partition); |
| } |
| |
| /** |
| * Returns the tab size used by the editor, which is deduced from the |
| * formatter preferences. |
| * |
| * @return the tab size as defined in the current formatter preferences |
| */ |
| private int getTabSize() { |
| return getCoreFormatterOption(4); |
| } |
| |
| /** |
| * Returns <code>true</code> if empty lines should be indented, |
| * <code>false</code> otherwise. |
| * |
| * @return <code>true</code> if empty lines should be indented, |
| * <code>false</code> otherwise |
| */ |
| private boolean indentEmptyLines() { |
| return STPDefaultCodeFormatterConstants.TRUE |
| .equals(getCoreFormatterOption()); |
| } |
| |
| /** |
| * Returns <code>true</code> if line comments at column 0 should be indented |
| * inside, <code>false</code> otherwise. |
| * |
| * @return <code>true</code> if line comments at column 0 should be indented |
| * inside, <code>false</code> otherwise. |
| */ |
| private boolean indentInsideLineComments() { |
| return STPDefaultCodeFormatterConstants.TRUE |
| .equals(getCoreFormatterOption()); |
| } |
| |
| /** |
| * Returns the possibly project-specific core preference defined under |
| * <code>key</code>. |
| * |
| * @param key |
| * the key of the preference |
| * @return the value of the preference |
| */ |
| private String getCoreFormatterOption() { |
| return "false"; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the possibly project-specific core preference defined under |
| * <code>key</code>, or <code>def</code> if the value is not a integer. |
| * |
| * @param def |
| * the default value |
| * @return the value of the preference |
| */ |
| private int getCoreFormatterOption(int def) { |
| try { |
| return Integer.parseInt(getCoreFormatterOption()); |
| } catch (NumberFormatException e) { |
| return def; |
| } |
| } |
| |
| /** |
| * Returns the <code>IProject</code> of the current editor input, or |
| * <code>null</code> if it cannot be found. |
| * |
| * @return the <code>IProject</code> of the current editor input, or |
| * <code>null</code> if it cannot be found |
| */ |
| private IProject getProject(ITextEditor editor) { |
| if (editor == null) |
| return null; |
| |
| IEditorInput input = editor.getEditorInput(); |
| if (input instanceof IFileEditorInput) |
| return ((IFileEditorInput) input).getFile().getProject(); |
| return null; |
| } |
| |
| /** |
| * Returns the editor's selection provider. |
| * |
| * @return the editor's selection provider or <code>null</code> |
| */ |
| private ISelectionProvider getSelectionProvider(ITextEditor editor) { |
| if (editor != null) { |
| return editor.getSelectionProvider(); |
| } |
| return null; |
| } |
| |
| /* |
| * @see org.eclipse.ui.texteditor.IUpdate#update() |
| */ |
| |
| /** |
| * Returns the document currently displayed in the editor, or |
| * <code>null</code> if none can be obtained. |
| * |
| * @return the current document or <code>null</code> |
| */ |
| private IDocument getDocument(ITextEditor editor) { |
| if (editor != null) { |
| IDocumentProvider provider = editor.getDocumentProvider(); |
| IEditorInput input = editor.getEditorInput(); |
| if (provider != null && input != null) |
| return provider.getDocument(input); |
| |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the selection on the editor or an invalid selection if none can |
| * be obtained. Returns never <code>null</code>. |
| * |
| * @return the current selection, never <code>null</code> |
| */ |
| private ITextSelection getSelection(ITextEditor editor) { |
| ISelectionProvider provider = getSelectionProvider(editor); |
| if (provider != null) { |
| ISelection selection = provider.getSelection(); |
| if (selection instanceof ITextSelection) |
| return (ITextSelection) selection; |
| } |
| |
| // null object |
| return TextSelection.emptySelection(); |
| } |
| } |