/******************************************************************************* * 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; } /** * 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(); } }