/*******************************************************************************
* Copyright (c) 2005 RadRails.org and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package com.aptana.ide.editor.erb;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Point;
import org.radrails.rails.ui.RailsUILog;
import org.radrails.rails.ui.RailsUIPlugin;
import com.aptana.ide.editor.erb.preferences.IPreferenceConstants;
/**
* Abstract <code>VerifyKeyListener</code> with some constants and helper
* methods. Subclasses implement specific peer closing behavior, such as
* matching close brackets to open brackets or matching <code>end</code> to
* <code>def</code>.
*
* @author mkent
*
*/
public class ExpressionCloser implements VerifyKeyListener, IPropertyChangeListener {
// Character constants
private static final char SINGLE_QUOTE_STRING = '\'';
private static final char DOUBLE_QUOTE_STRING = '"';
private static final String COMMENT_HASH = "#";
private SourceViewer fSourceViewer;
private boolean fEnabled;
/**
* Constructor.
*
* @param sourceViewer
* the <code>SourceViewer</code> that the closer operates on
*/
public ExpressionCloser(SourceViewer sourceViewer) {
fSourceViewer = sourceViewer;
fEnabled = ERBPlugin.getDefault().getPreferenceStore().getBoolean(IPreferenceConstants.EDITORS_AUTO_CLOSE);
RailsUIPlugin.getInstance().getPreferenceStore()
.addPropertyChangeListener(this);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent)
*/
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty().equals(IPreferenceConstants.EDITORS_AUTO_CLOSE)) {
fEnabled = ((Boolean) event.getNewValue()).booleanValue();
}
}
/**
* Helper method to move the cursor in the <code>SourceViewer</code>.
*
* @param cursorDelta
* the number of units to move the cursor, positive numbers will
* move it forward and negative numbers will move it backward
*/
protected void moveCursor(int cursorDelta) {
int widgetCursorPos = fSourceViewer.getTextWidget().getCaretOffset();
int docCursorPos = fSourceViewer.widgetOffset2ModelOffset(widgetCursorPos);
fSourceViewer.setSelectedRange(docCursorPos + cursorDelta, 0);
}
/**
* Helper method to determine if the current line in the
* <code>SourceViewer</code> is a Ruby comment.
*
* @return true if the current line is a Ruby comment, false otherwise
*/
protected boolean isCurrentLineRubyComment() {
try {
IDocument document = fSourceViewer.getDocument();
Point selection = fSourceViewer.getSelectedRange();
int cursorOffset = selection.x;
int lineNumber = document.getLineOfOffset(cursorOffset);
int lineLength = document.getLineLength(lineNumber);
int lineOffset = document.getLineOffset(lineNumber);
String lineText = document.get(lineOffset, lineLength);
if (lineText.trim().startsWith(COMMENT_HASH)) {
return true;
}
} catch (BadLocationException e) {
RailsUILog.logError("Bad location", e);
}
return false;
}
/**
* Helper method to determine if the cursor in the <code>SourceViewer</code>
* is currently inside a string literal.
*
* @return true if the cursor is inside a string, false otherwise
*/
protected boolean isCursorInsideString() {
return isCursorInsideString(DOUBLE_QUOTE_STRING) || isCursorInsideString(SINGLE_QUOTE_STRING);
}
/**
* @param lineText
* @param stringDelimiter
* @param cursorPos
* @return
*/
protected boolean isCursorInsideString(char stringDelimiter) {
try {
IDocument document = fSourceViewer.getDocument();
Point selection = fSourceViewer.getSelectedRange();
int cursorOffset = selection.x;
int lineNumber = document.getLineOfOffset(cursorOffset);
int lineLength = document.getLineLength(lineNumber);
int lineOffset = document.getLineOffset(lineNumber);
int cursorPos = cursorOffset - lineOffset;
String lineText = document.get(lineOffset, lineLength);
boolean loop = false;
int searchPos = 0;
do {
int openQuotePos = lineText.indexOf(stringDelimiter, searchPos);
int closeQuotePos = lineText.indexOf(stringDelimiter,
openQuotePos + 1);
// If there is a complete string literal on the line, check the
// cursor position
if ((openQuotePos > -1) && (closeQuotePos > -1)) {
// If the cursor is inside the string literal, return true
if ((cursorPos > openQuotePos)
&& (cursorPos <= closeQuotePos)) {
return true;
}
// Otherwise, check for the next string literal
else {
searchPos = closeQuotePos + 1;
loop = true;
}
}
loop = false;
} while (loop);
} catch (BadLocationException e) {
RailsUILog.logError("Bad location", e);
}
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent)
*/
public void verifyKey(VerifyEvent event) {
if (!fEnabled) {
return;
}
// Don't auto-close inside Ruby comments
if(isCurrentLineRubyComment()) {
return;
}
// Don't auto-close inside string literals
if(isCursorInsideString()) {
return;
}
IDocument document = fSourceViewer.getDocument();
Point selection = fSourceViewer.getSelectedRange();
int cursorOffset = selection.x;
int selectionLength = selection.y;
// If character is %, this is an open Ruby expression tag, complete it
if (event.character == '%') {
try {
char prevChar = document.getChar(cursorOffset - 1);
if (prevChar == '<') {
StringBuffer sb = new StringBuffer();
sb.append(event.character);
sb.append('%');
sb.append('>');
document.replace(cursorOffset, selectionLength, sb
.toString());
event.doit = false;
moveCursor(1);
}
} catch (BadLocationException e) {
RailsUILog.logError("Bad location", e);
}
}
}
}