/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain other free and open source software ("FOSS") code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.ide.editors.unified; import java.util.HashMap; import java.util.Map; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.DefaultPositionUpdater; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IPositionUpdater; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextPresentation; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.IVerticalRuler; import org.eclipse.jface.text.source.SourceViewerConfiguration; import org.eclipse.jface.text.source.projection.ProjectionViewer; import org.eclipse.swt.custom.ST; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Layout; import com.aptana.ide.core.IdeLog; import com.aptana.ide.core.StringUtils; import com.aptana.ide.core.Trace; import com.aptana.ide.editors.UnifiedEditorsPlugin; import com.aptana.ide.editors.unified.contentassist.IUnifiedContentAssistant; /** * */ public class UnifiedViewer extends ProjectionViewer implements IUnifiedViewer { private boolean hotkeyActivated = false; private TextPresentation presentation; /** * The text viewer's text triple click strategies */ protected Map<String, ITextTripleClickStrategy> fTripleClickStrategies; /** * Triple click connector. */ protected TripleClickConnector fTripleClickStrategyConnector; // **** NOTE **** // we are not using Projection Viewer in order to get shift left working (to shift until no // whitespace) // if this is problematic or 3.2 exposes that in a more intelligent way (or fixes that issue) we // can get rid // of UnifiedViewer, and revert one line of code in UnifiedEditor.createSourceViewer. // our only changes are prefixed with "// [APTANA]" /** * @param parent * @param ruler * @param overviewRuler * @param showsAnnotationOverview * @param styles */ public UnifiedViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, boolean showsAnnotationOverview, int styles) { super(parent, ruler, overviewRuler, showsAnnotationOverview, styles); } /** * @see org.eclipse.jface.text.source.SourceViewer#createLayout() */ protected Layout createLayout() { return new RulerLayout(0); } /** * Shifts a text block to the right or left using the specified set of prefix characters. The prefixes must start at * the beginning of the line. * * @param useDefaultPrefixes * says whether the configured default or indent prefixes should be used * @param right * says whether to shift to the right or the left * @deprecated use shift(boolean, boolean, boolean) instead */ protected void shift(boolean useDefaultPrefixes, boolean right) { shift(useDefaultPrefixes, right, false); } /** * @see org.eclipse.jface.text.TextViewer#createTextWidget(org.eclipse.swt.widgets.Composite, int) */ protected StyledText createTextWidget(Composite parent, int styles) { return new StyledText(parent, styles) { boolean swap = false; public void setLineBackground(int startLine, int lineCount, Color background) { swap = true; super.setLineBackground(startLine, lineCount, background); swap = false; } public boolean isListening(int eventType) { if (swap && eventType == 3001) { return false; } return super.isListening(eventType); } public void invokeAction(int action) { if (getWordWrap() && ST.LINE_DOWN == action) { int previous = getCaretOffset(); super.invokeAction(action); if (previous == getCaretOffset()) { int line = getLineAtOffset(previous); if (line + 1 < getLineCount()) { setCaretOffset(getOffsetAtLine(line + 1)); } } } else { super.invokeAction(action); } } }; } /** * Shifts a text block to the right or left using the specified set of prefix characters. If white space should be * ignored the prefix characters must not be at the beginning of the line when shifting to the left. There may be * whitespace in front of the prefixes. * * @param useDefaultPrefixes * says whether the configured default or indent prefixes should be used * @param right * says whether to shift to the right or the left * @param ignoreWhitespace * says whether whitespace in front of prefixes is allowed * @since 2.0 */ protected void shift(boolean useDefaultPrefixes, boolean right, boolean ignoreWhitespace) { if (fUndoManager != null) { fUndoManager.beginCompoundChange(); } setRedraw(false); startSequentialRewriteMode(true); IDocument d = getDocument(); Map partitioners = null; try { Point selection = getSelectedRange(); IRegion block = getTextBlockFromSelection(selection); ITypedRegion[] regions = TextUtilities.computePartitioning(d, getDocumentPartitioning(), block.getOffset(), block.getLength(), false); int lineCount = 0; int[] lines = new int[regions.length * 2]; // [start line, end line, start line, end // line, ...] for (int i = 0, j = 0; i < regions.length; i++, j += 2) { // start line of region lines[j] = getFirstCompleteLineOfRegion(regions[i]); // end line of region int length = regions[i].getLength(); int offset = regions[i].getOffset() + length; if (length > 0) { offset--; } lines[j + 1] = (lines[j] == -1 ? -1 : d.getLineOfOffset(offset)); lineCount += lines[j + 1] - lines[j] + 1; } if (lineCount >= 20) { partitioners = TextUtilities.removeDocumentPartitioners(d); } // Remember the selection range. IPositionUpdater positionUpdater = new ShiftPositionUpdater(SHIFTING); Position rememberedSelection = new Position(selection.x, selection.y); d.addPositionCategory(SHIFTING); d.addPositionUpdater(positionUpdater); try { d.addPosition(SHIFTING, rememberedSelection); } catch (BadPositionCategoryException ex) { // should not happen } // Perform the shift operation. Map map = (useDefaultPrefixes ? fDefaultPrefixChars : fIndentChars); for (int i = 0, j = 0; i < regions.length; i++, j += 2) { String[] prefixes = (String[]) selectContentTypePlugin(regions[i].getType(), map); if (prefixes != null && prefixes.length > 0 && lines[j] >= 0 && lines[j + 1] >= 0) { if (right) { shiftRight(lines[j], lines[j + 1], prefixes[0], d); } else { shiftLeft(lines[j], lines[j + 1], prefixes, ignoreWhitespace, d); } } } // Restore the selection. setSelectedRange(rememberedSelection.getOffset(), rememberedSelection.getLength()); try { d.removePositionUpdater(positionUpdater); d.removePositionCategory(SHIFTING); } catch (BadPositionCategoryException ex) { // should not happen } } catch (BadLocationException x) { } finally { if (partitioners != null) { TextUtilities.addDocumentPartitioners(d, partitioners); } stopSequentialRewriteMode(); setRedraw(true); if (fUndoManager != null) { fUndoManager.endCompoundChange(); } } } /** * Shifts the specified lines to the right inserting the given prefix at the beginning of each line * * @param prefix * the prefix to be inserted * @param startLine * the first line to shift * @param endLine * the last line to shift * @param document * @since 2.0 */ public static void shiftRight(int startLine, int endLine, String prefix, IDocument document) { try { while (startLine <= endLine) { document.replace(document.getLineOffset(startLine++), 0, prefix); } } catch (BadLocationException x) { if (TRACE_ERRORS) { IdeLog.logError(UnifiedEditorsPlugin.getDefault(), "TextViewer.shiftRight: BadLocationException", x); //$NON-NLS-1$ } } } /** * Shifts the specified lines to the right or to the left. On shifting to the right it insert * <code>prefixes[0]</code> at the beginning of each line. On shifting to the left it tests whether each of the * specified lines starts with one of the specified prefixes and if so, removes the prefix. * * @param startLine * the first line to shift * @param endLine * the last line to shift * @param prefixes * the prefixes to be used for shifting * @param ignoreWhitespace * <code>true</code> if whitespace should be ignored, <code>false</code> otherwise * @param document * The document * @since 2.0 */ public static void shiftLeft(int startLine, int endLine, String[] prefixes, boolean ignoreWhitespace, IDocument document) { try { IRegion[] occurrences = new IRegion[endLine - startLine + 1]; // find all the first occurrences of prefix in the given lines for (int i = 0; i < occurrences.length; i++) { IRegion line = document.getLineInformation(startLine + i); String text = document.get(line.getOffset(), line.getLength()); int index = -1; int[] found = TextUtilities.indexOf(prefixes, text, 0); if (found[0] != -1) { if (ignoreWhitespace) { String s = document.get(line.getOffset(), found[0]); s = s.trim(); if (s.length() == 0) { index = line.getOffset() + found[0]; } } else if (found[0] == 0) { index = line.getOffset(); } } if (index > -1) { // remember where prefix is in line, so that it can be removed int length = prefixes[found[1]].length(); if (length == 0 && !ignoreWhitespace && line.getLength() > 0) { // found a non-empty line which cannot be shifted // return; // [APTANA] occurrences[i] = new Region(index, 0); } else { occurrences[i] = new Region(index, length); } } else { // found a line which cannot be shifted // return; // [APTANA] occurrences[i] = new Region(index, 0); } } // OK - change the document int decrement = 0; for (int i = 0; i < occurrences.length; i++) { IRegion r = occurrences[i]; // [APTANA] if (r.getLength() == 0) { continue; } document.replace(r.getOffset() - decrement, r.getLength(), StringUtils.EMPTY); decrement += r.getLength(); } } catch (BadLocationException x) { if (TRACE_ERRORS) { Trace.info("TextViewer.shiftLeft: BadLocationException"); //$NON-NLS-1$ } } } /** * Creates a region describing the text block (something that starts at the beginning of a line) completely * containing the current selection. * * @param selection * the selection to use * @return the region describing the text block comprising the given selection * @since 2.0 */ private IRegion getTextBlockFromSelection(Point selection) { try { IDocument document = getDocument(); IRegion line = document.getLineInformationOfOffset(selection.x); int length = selection.y == 0 ? line.getLength() : selection.y + (selection.x - line.getOffset()); return new Region(line.getOffset(), length); } catch (BadLocationException x) { } return null; } /** * Returns the index of the first line whose start offset is in the given text range. * * @param region * the text range in characters where to find the line * @return the first line whose start index is in the given range, -1 if there is no such line */ private int getFirstCompleteLineOfRegion(IRegion region) { try { IDocument d = getDocument(); int startLine = d.getLineOfOffset(region.getOffset()); int offset = d.getLineOffset(startLine); if (offset >= region.getOffset()) { return startLine; } offset = d.getLineOffset(startLine + 1); return (offset > region.getOffset() + region.getLength() ? -1 : startLine + 1); } catch (BadLocationException x) { } return -1; } /** * Selects from the given <code>plug-ins</code> this one which is registered for the given content * <code>type</code>. * * @param type * the type to be used as lookup key * @param plugins * the table to be searched * @return the plug-in in the map for the given content type */ private Object selectContentTypePlugin(String type, Map plugins) { if (plugins == null) { return null; } return plugins.get(type); } /** * This position updater is used to keep the selection during text shift operations. */ static class ShiftPositionUpdater extends DefaultPositionUpdater { /** * Creates the position updater for the given category. * * @param category * the category this updater takes care of */ protected ShiftPositionUpdater(String category) { super(category); } /** * If an insertion happens at the selection's start offset, the position is extended rather than shifted. */ protected void adaptToInsert() { int myStart = fPosition.offset; int myEnd = fPosition.offset + fPosition.length - 1; myEnd = Math.max(myStart, myEnd); int yoursStart = fOffset; int yoursEnd = fOffset + fReplaceLength - 1; yoursEnd = Math.max(yoursStart, yoursEnd); if (myEnd < yoursStart) { return; } if (myStart <= yoursStart) { fPosition.length += fReplaceLength; return; } if (myStart > yoursStart) { fPosition.offset += fReplaceLength; } } } /** * @return isHotkeyActivated */ public boolean isHotkeyActivated() { return hotkeyActivated; } /** * @param value */ public void setHotkeyActivated(boolean value) { hotkeyActivated = value; } /** * Closes the open content assist window */ public void closeContentAssist() { if (fContentAssistant != null && fContentAssistant instanceof IUnifiedContentAssistant) { ((IUnifiedContentAssistant) fContentAssistant).hide(); } } /** * {@inheritDoc} */ public void activatePlugins() { super.activatePlugins(); } /** * Sets text triple click strategy. * * @param strategy - * strategy to set. * @param contentType - * content type. */ public void setTextTripleClickStrategy(ITextTripleClickStrategy strategy, String contentType) { if (strategy != null) { if (fTripleClickStrategies == null) { fTripleClickStrategies = new HashMap<String, ITextTripleClickStrategy>(); } fTripleClickStrategies.put(contentType, strategy); } else if (fTripleClickStrategies != null) { fTripleClickStrategies.remove(contentType); } } /** * {@inheritDoc} */ public void configure(SourceViewerConfiguration configuration) { super.configure(configuration); if (configuration instanceof UnifiedConfiguration) { UnifiedConfiguration conf = (UnifiedConfiguration) configuration; // install content type specific plug-ins String[] types = configuration.getConfiguredContentTypes(this); for (int i = 0; i < types.length; i++) { String t = types[i]; setTextTripleClickStrategy(conf.getTripleClickStrategy(this, t), t); } } activateTripleClickStrategies(); } /** * Activates triple click strategies. */ private void activateTripleClickStrategies() { if (fTripleClickStrategies != null && !fTripleClickStrategies.isEmpty() && fTripleClickStrategyConnector == null) { fTripleClickStrategyConnector = new TripleClickConnector() { @Override public void mouseTripleClick(MouseEvent e) { ITextTripleClickStrategy s = (ITextTripleClickStrategy) selectContentTypePlugin( getSelectedRange().x, fTripleClickStrategies); s.tripleClicked(UnifiedViewer.this); } }; getTextWidget().addMouseListener(fTripleClickStrategyConnector); } } /* * @see ITextViewer#changeTextPresentation(TextPresentation, boolean) */ public void changeTextPresentation(TextPresentation presentation, boolean controlRedraw) { super.changeTextPresentation(presentation, controlRedraw); this.presentation=presentation; } public TextPresentation getTextPresentation() { return presentation; } }