/**
* Copyright (c) 2005-2006 Aptana, Inc.
*
* 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. If redistributing this code,
* this entire header must remain intact.
*/
package com.aptana.ide.editors.formatting;
import java.util.Arrays;
import java.util.Stack;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.link.ILinkedModeListener;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditor;
import org.eclipse.ui.texteditor.ITextEditorExtension3.InsertMode;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.epl.Activator;
/**
* @author Paul Colton
*/
public abstract class UnifiedBracketInserterBase implements VerifyKeyListener, ILinkedModeListener
{
/**
* sourceViewer
*/
protected ISourceViewer sourceViewer;
/**
* UnifiedBracketInserter
*
* @param sourceViewer
*/
public UnifiedBracketInserterBase(ISourceViewer sourceViewer)
{
this.sourceViewer = sourceViewer;
}
/**
* hasPeerCharacter
*
* @param character
* @return boolean
*/
public static boolean hasPeerCharacter(char character)
{
switch (character)
{
case '(':
case ')':
case '<':
case '>':
case '[':
case ']':
case '"':
case '\'':
case '{':
case '}':
{
return true;
}
default:
{
return false;
}
}
}
/**
* getPeerCharacter
*
* @param character
* @return char
*/
public static char getPeerCharacter(char character)
{
switch (character)
{
case '(':
return ')';
case ')':
return '(';
case '<':
return '>';
case '>':
return '<';
case '[':
return ']';
case ']':
return '[';
case '"':
return character;
case '\'':
return character;
case '{':
return '}';
case '}':
return '{';
default:
throw new IllegalArgumentException();
}
}
/**
* This checks if a string has a balanced amount of the given char eg:() [] "" '' This is true if the string is
* balanced after the character typed, but before the potential auto-insert.
*
* @param source
* @param charToCheck
* @param isInserting
* @return Returns true if the string is balanced after the character typed, but before the potential auto-insert.
*/
public static boolean isStringBalanced(String source, char charToCheck, boolean isInserting)
{
if (!hasPeerCharacter(charToCheck))
{
return true;
}
char peer = getPeerCharacter(charToCheck);
char[] sourceChars = source.toCharArray();
int charCount = 0;
int peerCount = 0;
for (int i = 0; i < sourceChars.length; i++)
{
if (sourceChars[i] == charToCheck)
{
charCount++;
}
else if (sourceChars[i] == peer)
{
peerCount++;
}
}
// for strings and quotes just check for even and odd
// we don't care about insert vs delete in this case as
// we just want to know if one char will make it even
if (charToCheck == '"' || charToCheck == '\'')
{
double firstChangeOnly = charCount + 1;
return (firstChangeOnly % 2) == 0;
}
int insertCount = isInserting ? 1 : -1;
return charCount + insertCount == peerCount;
}
/**
* fCloseBrackets
*/
protected boolean fCloseBrackets = true;
/**
* fCloseStrings
*/
protected boolean fCloseStrings = true;
/**
* fCloseAngularBrackets
*/
protected boolean fCloseAngularBrackets = false;
private final String CATEGORY = toString();
private IPositionUpdater fUpdater = new ExclusivePositionUpdater(CATEGORY);
private Stack fBracketLevelStack = new Stack();
/**
* Set close bracket enabled
*
* @param enabled
*/
public void setCloseBracketsEnabled(boolean enabled)
{
fCloseBrackets = enabled;
}
/**
* Set close strings enabled
*
* @param enabled
*/
public void setCloseStringsEnabled(boolean enabled)
{
fCloseStrings = enabled;
}
/**
* Set close angular brackets enabled
*
* @param enabled
*/
public void setCloseAngularBracketsEnabled(boolean enabled)
{
fCloseAngularBrackets = enabled;
}
/**
* Is angualar introducer
*
* @param identifier
* @return - true if angular introducer
*/
protected boolean isAngularIntroducer(String identifier)
{
return identifier.length() > 0
&& (Character.isUpperCase(identifier.charAt(0)) || identifier.startsWith("final") //$NON-NLS-1$
|| identifier.startsWith("public") //$NON-NLS-1$
|| identifier.startsWith("public") //$NON-NLS-1$
|| identifier.startsWith("protected") //$NON-NLS-1$
|| identifier.startsWith("private")); //$NON-NLS-1$
}
/**
* Gets the insert mode
*
* @return - insert mode
*/
public InsertMode getInsertMode()
{
return AbstractDecoratedTextEditor.SMART_INSERT;
}
/**
* @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent)
*/
public void verifyKey(VerifyEvent event)
{
// early pruning to slow down normal typing as little as possible
if (!event.doit || getInsertMode() != AbstractDecoratedTextEditor.SMART_INSERT)
{
return;
}
// Don't insert if it's not the proper character
if (!isAutoInsertCharacter(event.character))
{
return;
}
// Don't insert if auto-insert is not enabled
if (!isAutoInsertEnabled())
{
return;
}
IDocument document = sourceViewer.getDocument();
final Point selection = sourceViewer.getSelectedRange();
final int offset = selection.x;
final int length = selection.y;
try
{
if (!isValidAutoInsertLocation(event.character, offset, length))
{
return;
}
final char character = event.character;
final char closingCharacter = getPeerCharacter(character);
final StringBuffer buffer = new StringBuffer();
buffer.append(character);
buffer.append(closingCharacter);
// We can only draw the green line if a character is after the inserted characters
// This code inserts a new line if the inserted text is at the end of the document and without this no green
// bar would appear
if (offset == document.getLength())
{
String delim = null;
if (document instanceof IDocumentExtension4)
{
delim = ((IDocumentExtension4) document).getDefaultLineDelimiter();
}
if (delim == null)
{
delim = System.getProperty("line.separator", "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
}
buffer.append(delim);
}
document.replace(offset, length, buffer.toString());
BracketLevel level = new BracketLevel();
fBracketLevelStack.push(level);
LinkedPositionGroup group = new LinkedPositionGroup();
group.addPosition(new LinkedPosition(document, offset + 1, 0, LinkedPositionGroup.NO_STOP));
LinkedModeModel model = new LinkedModeModel();
model.addLinkingListener(this);
model.addGroup(group);
model.forceInstall();
level.fOffset = offset;
level.fLength = 2;
// set up position tracking for our magic peers
if (fBracketLevelStack.size() == 1)
{
document.addPositionCategory(CATEGORY);
document.addPositionUpdater(fUpdater);
}
level.fFirstPosition = new Position(offset, 1);
level.fSecondPosition = new Position(offset + 1, 1);
document.addPosition(CATEGORY, level.fFirstPosition);
document.addPosition(CATEGORY, level.fSecondPosition);
level.fUI = new EditorLinkedModeUI(model, sourceViewer);
level.fUI.setSimpleMode(true);
level.fUI.setExitPolicy(new ExitPolicy(closingCharacter, getEscapeCharacter(closingCharacter),
fBracketLevelStack));
level.fUI.setExitPosition(sourceViewer, offset + 2, 0, Integer.MAX_VALUE);
level.fUI.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
level.fUI.enter();
IRegion newSelection = level.fUI.getSelectedRegion();
sourceViewer.setSelectedRange(newSelection.getOffset(), newSelection.getLength());
event.doit = doEvent(event);
triggerAssistPopup(event);
}
catch (BadLocationException e)
{
IdeLog.logError(Activator.getDefault(), Messages.UnifiedBracketInserterBase_ERR_BadLocation, e);
}
catch (BadPositionCategoryException e)
{
IdeLog.logError(Activator.getDefault(), Messages.UnifiedBracketInserterBase_ERR_BadLocation, e);
}
}
/**
* Alter the event before insertion
*
* @param event
* @return - returns false by default
*/
protected boolean doEvent(VerifyEvent event)
{
return false;
}
/**
* @see org.eclipse.jface.text.link.ILinkedModeListener#left(org.eclipse.jface.text.link.LinkedModeModel, int)
*/
public void left(LinkedModeModel environment, int flags)
{
final BracketLevel level = (BracketLevel) fBracketLevelStack.pop();
if (flags != ILinkedModeListener.EXTERNAL_MODIFICATION)
{
return;
}
// remove brackets
final IDocument document = sourceViewer.getDocument();
if (document instanceof IDocumentExtension)
{
IDocumentExtension extension = (IDocumentExtension) document;
extension.registerPostNotificationReplace(null, new IDocumentExtension.IReplace()
{
public void perform(IDocument document, IDocumentListener owner)
{
if ((level.fFirstPosition.isDeleted || level.fFirstPosition.length == 0)
&& !level.fSecondPosition.isDeleted
&& level.fSecondPosition.offset == level.fFirstPosition.offset)
{
try
{
document.replace(level.fSecondPosition.offset, level.fSecondPosition.length, null);
}
catch (BadLocationException e)
{
IdeLog.logError(Activator.getDefault(), Messages.UnifiedBracketInserterBase_ERR_BadLocation, e);
}
}
if (fBracketLevelStack.size() == 0)
{
document.removePositionUpdater(fUpdater);
try
{
document.removePositionCategory(CATEGORY);
}
catch (BadPositionCategoryException e)
{
IdeLog.logError(Activator.getDefault(), Messages.UnifiedBracketInserterBase_ERR_BadLocation, e);
}
}
}
});
}
}
/**
* @see org.eclipse.jface.text.link.ILinkedModeListener#suspend(org.eclipse.jface.text.link.LinkedModeModel)
*/
public void suspend(LinkedModeModel environment)
{
}
/**
* @see org.eclipse.jface.text.link.ILinkedModeListener#resume(org.eclipse.jface.text.link.LinkedModeModel, int)
*/
public void resume(LinkedModeModel environment, int flags)
{
}
/**
* Position updater that takes any changes at the borders of a position to not belong to the position.
*
* @since 3.0
*/
private static class ExclusivePositionUpdater implements IPositionUpdater
{
/** The position category. */
private final String fCategory;
/**
* Creates a new updater for the given <code>category</code>.
*
* @param category
* the new category.
*/
public ExclusivePositionUpdater(String category)
{
fCategory = category;
}
/**
* @see org.eclipse.jface.text.IPositionUpdater#update(org.eclipse.jface.text.DocumentEvent)
*/
public void update(DocumentEvent event)
{
int eventOffset = event.getOffset();
int eventOldLength = event.getLength();
int eventNewLength = event.getText() == null ? 0 : event.getText().length();
int deltaLength = eventNewLength - eventOldLength;
try
{
Position[] positions = event.getDocument().getPositions(fCategory);
for (int i = 0; i != positions.length; i++)
{
Position position = positions[i];
if (position.isDeleted())
{
continue;
}
int offset = position.getOffset();
int length = position.getLength();
int end = offset + length;
if (offset >= eventOffset + eventOldLength)
{
// position comes
// after change - shift
position.setOffset(offset + deltaLength);
}
else if (end <= eventOffset)
{
// position comes way before change -
// leave alone
}
else if (offset <= eventOffset && end >= eventOffset + eventOldLength)
{
// event completely internal to the position - adjust
// length
position.setLength(length + deltaLength);
}
else if (offset < eventOffset)
{
// event extends over end of position - adjust length
int newEnd = eventOffset;
position.setLength(newEnd - offset);
}
else if (end > eventOffset + eventOldLength)
{
// event extends from before position into it - adjust
// offset
// and length
// offset becomes end of event, length adjusted
// accordingly
int newOffset = eventOffset + eventNewLength;
position.setOffset(newOffset);
position.setLength(end - newOffset);
}
else
{
// event consumes the position - delete it
position.delete();
}
}
}
catch (BadPositionCategoryException e)
{
// ignore and return
}
}
/**
* Returns the position category.
*
* @return the position category
*/
public String getCategory()
{
return fCategory;
}
}
/**
* @author Ingo Muschenetz
*/
private static class BracketLevel
{
int fOffset;
int fLength;
LinkedModeUI fUI;
Position fFirstPosition;
Position fSecondPosition;
}
private static char getEscapeCharacter(char character)
{
switch (character)
{
case '"':
case '\'':
return '\\';
default:
return 0;
}
}
/**
* @author Ingo Muschenetz
*/
private class ExitPolicy implements IExitPolicy
{
final char fExitCharacter;
final char fEscapeCharacter;
final Stack fStack;
final int fSize;
/**
* Creates a new exit policy
*
* @param exitCharacter
* @param escapeCharacter
* @param stack
*/
public ExitPolicy(char exitCharacter, char escapeCharacter, Stack stack)
{
fExitCharacter = exitCharacter;
fEscapeCharacter = escapeCharacter;
fStack = stack;
fSize = fStack.size();
}
/**
* Do exit
*
* @param model
* @param event
* @param offset
* @param length
* @return the exit flags
*/
public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length)
{
if (fSize == fStack.size() && !isMasked(offset))
{
if (event.character == fExitCharacter)
{
BracketLevel level = (BracketLevel) fStack.peek();
if (level.fFirstPosition.offset > offset || level.fSecondPosition.offset < offset)
{
return null;
}
if (level.fSecondPosition.offset == offset && length == 0)
{
// don't enter the character if if its the closing peer
triggerAssistClose(event);
return new ExitFlags(ILinkedModeListener.UPDATE_CARET, false);
}
}
// when entering an anonymous class between the parenthesis', we
// don't want,
// to jump after the closing parenthesis when return is pressed
if (event.character == SWT.CR && offset > 0)
{
IDocument document = sourceViewer.getDocument();
try
{
if (document.getChar(offset - 1) == '{')
{
return new ExitFlags(ILinkedModeListener.EXIT_ALL, true);
}
}
catch (BadLocationException e)
{
}
}
}
return null;
}
private boolean isMasked(int offset)
{
IDocument document = sourceViewer.getDocument();
try
{
return fEscapeCharacter == document.getChar(offset - 1);
}
catch (BadLocationException e)
{
}
return false;
}
}
/**
* Is this location a valid place to insert the specified character?
*
* @param character
* The character inserted
* @param offset
* The offset of the insert
* @param length
* The length of the insertion
* @return - true by default
*/
protected boolean isValidAutoInsertLocation(char character, int offset, int length)
{
return true;
}
/**
* @param c
* @return - true if auto insert character
*/
private boolean isAutoInsertCharacter(char c)
{
char[] arr = getAutoInsertCharacters();
Arrays.sort(arr);
int val = Arrays.binarySearch(arr, c);
return val >= 0;
}
/**
* getAutoInsertCharacters
*
* @return char[]
*/
protected char[] getAutoInsertCharacters()
{
return new char[] { '"', '\'', '(', '[', '{' };
// return new char[] { '"', '\'', '<', '(', '[', '{' };
}
/**
* isAutoInsertEnabled
*
* @return boolean
*/
protected boolean isAutoInsertEnabled()
{
return true;
}
/**
* Forces code assist to appear assuming it can be shown
*
* @param event
*/
protected void triggerAssistPopup(VerifyEvent event)
{
}
/**
* Forces code assist to close
*
* @param event
*/
protected void triggerAssistClose(VerifyEvent event)
{
}
}