blob: d8fe5c6c82ef2f626767159a250f5297c2acf1e6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2018 Borland Software Corporation and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Borland Software Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.m2m.internal.qvt.oml.editor.ui;
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.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.link.EditorLinkedModeUI;
/**
* This class is used for automatic bracket (and dollar sign) closing and for
* replacement of quotation marks. Most parts are copied from the Eclipse JDT.
*/
public class BracketInserter implements VerifyKeyListener, ILinkedModeListener {
private class ExitPolicy implements IExitPolicy {
final char fExitCharacter;
final char fEscapeCharacter;
final Stack<BracketLevel> fStack;
final int fSize;
final ISourceViewer sourceViewer;
public ExitPolicy(char exitCharacter, char escapeCharacter, Stack<BracketLevel> stack, ISourceViewer viewer) {
fExitCharacter = exitCharacter;
fEscapeCharacter = escapeCharacter;
fStack = stack;
fSize = fStack.size();
sourceViewer = viewer;
}
/*
* @see org.eclipse.jdt.internal.ui.text.link.LinkedPositionUI.ExitPolicy#doExit(org.eclipse.jdt.internal.ui.text.link.LinkedPositionManager,
* org.eclipse.swt.events.VerifyEvent, int, int)
*/
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
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;
}
}
private static class BracketLevel {
LinkedModeUI fUI;
Position fFirstPosition;
Position fSecondPosition;
}
/**
* 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
}
}
}
public BracketInserter(ISourceViewer viewer) {
mySourceViewer = viewer;
}
private static boolean hasPeerClosingCharacter(char character) {
return character == '(' || character == '[' || character == '\'' || character == '\"';
}
private static char getPeerClosingCharacter(char character) {
switch (character) {
case '(':
return ')';
case '[':
return ']';
case '\'':
return '\'';
case '\"':
return '\"';
}
throw new IllegalArgumentException();
}
private static boolean isClosingCharacter(char character) {
return character == ')' || character == ']' || character == '\'';
}
public void verifyKey(VerifyEvent event) {
char character = event.character;
if (!hasPeerClosingCharacter(character)) {
return;
}
IDocument document = mySourceViewer.getDocument();
Point selection = mySourceViewer.getSelectedRange();
int offset = selection.x;
int length = selection.y;
try {
char next = ' ';
try {
next = document.getChar(offset);
} catch (BadLocationException e) {
// ignore
}
if (!Character.isWhitespace(next) && !isClosingCharacter(next)) {
return;
}
char closingCharacter = getPeerClosingCharacter(character);
StringBuilder buffer = new StringBuilder();
buffer.append(character);
buffer.append(closingCharacter);
document.replace(offset, length, buffer.toString());
// The code below does the fancy "templateish" enter-to-exit-braces
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();
// 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, mySourceViewer);
level.fUI.setSimpleMode(true);
level.fUI.setExitPolicy(new ExitPolicy(closingCharacter, (char) 0, fBracketLevelStack, mySourceViewer));
level.fUI.setExitPosition(mySourceViewer, offset + 2, 0, Integer.MAX_VALUE);
level.fUI.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
level.fUI.enter();
IRegion newSelection = level.fUI.getSelectedRegion();
mySourceViewer.setSelectedRange(newSelection.getOffset(), newSelection.getLength());
event.doit = false;
} catch (BadLocationException e) {
} catch (BadPositionCategoryException e) {
}
}
public void left(LinkedModeModel environment, int flags) {
final BracketLevel level = (BracketLevel) fBracketLevelStack.pop();
if (flags != ILinkedModeListener.EXTERNAL_MODIFICATION) {
return;
}
// remove brackets
final IDocument document = mySourceViewer.getDocument();
if (document instanceof IDocumentExtension) {
IDocumentExtension extension = (IDocumentExtension) document;
extension.registerPostNotificationReplace(null, new IDocumentExtension.IReplace() {
public void perform(IDocument d, 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) {
// ignore
}
}
if (fBracketLevelStack.size() == 0) {
document.removePositionUpdater(fUpdater);
try {
document.removePositionCategory(CATEGORY);
} catch (BadPositionCategoryException e) {
// ignore
}
}
}
});
}
}
public void suspend(LinkedModeModel environment) {
}
public void resume(LinkedModeModel environment, int flags) {
}
private final String CATEGORY = toString();
private IPositionUpdater fUpdater = new ExclusivePositionUpdater(CATEGORY);
private Stack<BracketLevel> fBracketLevelStack = new Stack<BracketLevel>();
private final ISourceViewer mySourceViewer;
}