/**
* Copyright (c) 2009, 2010 Mark Feber, MulgaSoft
*
* 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
*/
package com.mulgasoft.emacsplus.execute;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.EmacsPlusUtils;
import com.mulgasoft.emacsplus.MarkUtils;
/**
* @author Mark Feber - initial API and implementation
*/
public class ColumnSupport {
protected final static String CR = "\n"; //$NON-NLS-1$
protected final static String EMPTY_STR = ""; //$NON-NLS-1$
private int tabWidth = 4;
private String eol = CR;
private boolean insertSpaces = false;
public ColumnSupport(IDocument document, Control widget) {
setUp(document,widget);
}
public ColumnSupport(IDocument document, ITextEditor editor) {
this(document,MarkUtils.getTextWidget(editor));
}
// It is not possible AFAIK to unwind the various preferences in Eclipse that determine this
// and set; some of the known preferences are:
// Preferences -> Editors -> Text Editors;
// Preferences -> Java -> Code Style -> Formatter
// C++, Javascript and other Language specific formatters
// so use Eclipse global preference
/**
* Get the Eclipse global preference value that controls spacing/tabs
*
* @return true if spaces set
*/
public static final boolean isSpacesForTabs() {
boolean result = false;
// This is the global editor preference
result = EditorsUI.getPreferenceStore().getBoolean(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SPACES_FOR_TABS);
return result;
}
protected void setUp(IDocument document, Control widget) {
// set up for length processing
eol = EmacsPlusUtils.getEol(document);
tabWidth = ((widget instanceof StyledText) ? ((StyledText)widget).getTabs() : ((Text)widget).getTabs());
insertSpaces = isSpacesForTabs();
}
public String getEol(){
return eol;
}
public int getTabWidth() {
return tabWidth;
}
/**
* Insert whitespace (tabs or spaces as appropriate) into document at off
* When replacing, emacs clears the segment if the line length is less than the column
*
* @param document
* @param column the current column of offset
* @param offset the point to begin the insertion
* @param colLen the length, in columns, of the insertion
* @param replace replace if true, else add
*
* @return the StringBuilder for length testing
*
* @throws BadLocationException
*/
public String insertSpaces(IDocument document, int column, int offset, int colLen, boolean replace, boolean force) throws BadLocationException {
String spaces;
IRegion lineInfo = document.getLineInformationOfOffset(offset);
// get the correct number of characters to traverse
int seglen = lineInfo.getLength() - (offset - lineInfo.getOffset());
// get the correct offset/column
IRegion colEnd = getColumn(document, offset, seglen, colLen);
if (replace && colEnd.getLength() < colLen ) {
if (colEnd.getOffset() != offset+seglen) {
// line is long enough but spacing in line does not match with required column
spaces = getSpaces(column, colEnd.getLength()).toString();
} else {
spaces = EMPTY_STR;
}
} else {
spaces = getSpaces(column, colLen).toString();
}
// insert into document at offset
document.replace(offset, (replace ? colEnd.getOffset() - offset : 0), spaces.toString());
return spaces;
}
/**
* Get whitespace as appropriate
*
* @param column the starting column
* @param colLen the column length
* @return whitespace of column length
*/
public String getSpaces(int column, int colLen) {
int count = colLen;
int tabWidth = getTabWidth();
StringBuilder spaces = new StringBuilder(count);
int prefix = column % tabWidth;
if (prefix != 0) {
prefix = tabWidth - prefix;
if (count > prefix) {
for (int i = 0; i < prefix; i++) {
spaces.append(' ');
}
count -= prefix;
}
}
int x = count / tabWidth;
for (int i=0; i < x; i++) {
if (insertSpaces) {
for (int j=0; j < tabWidth; j++) {
spaces.append(' ');
}
} else {
spaces.append('\t');
}
}
x = count % tabWidth;
for (int i=0; i < x; i++) {
spaces.append(' ');
}
return spaces.toString();
}
/**
* Get the insert position of the current line of the rectangle.
* Updates the line with spaces if necessary
*
* @param document
* @param offset an offset in the current line; cursor offset on initialization request
* @param column the start column position of the rectangle; or -1 on initialization request
* @return an IRegion(insert position, charLen)
*/
public IRegion getInsertPosition(IDocument document, int offset, int column, boolean force) {
IRegion result = null;
try {
IRegion reg = document.getLineInformationOfOffset(offset);
int off = reg.getOffset();
int numChars;
if (column == -1) {
column = Integer.MAX_VALUE;
numChars = offset - off;
} else {
numChars = reg.getLength();
}
result = getColumn(document, off, numChars, column,true);
} catch (BadLocationException e) {
result = null;
}
return result;
}
/**
* @param document
* @param offset - the initial offset on the line
* @param numChars - the number of characters from offset to eol
* @param column - the target column or Integer.MAX_VALUE to compute it
* @return a region containing the target offset and the column count from offset
*/
public IRegion getColumn(IDocument document, int offset, int numChars, int column) {
return getColumn(document, offset, numChars, column, false);
}
/**
* Determine the offset and column that corresponds to the column passed in
* If the line doesn't contain enough columns, or the offset position doesn't
* correspond to the column return closest offset/column information less than column
* Unless force is true, in which case fill the document to the length
*
* @param document
* @param offset initial offset on the line
* @param numChars number of characters to eol
* @param column target column (if Integer.MAX_VALUE, then result is the computed column)
* @param force flag to force justify
* @return the offset and number of columns from initial offset of the nearest or corresponding position
*/
protected IRegion getColumn(IDocument document, int offset, int numChars, int column, boolean force) {
int tabWidth = getTabWidth();
int count = 0;
int prevCount = 0;
int off = offset;
int lastOff = offset;
try {
for (int i=0; i < numChars; i++, off++) {
lastOff = off;
prevCount = count;
switch (document.getChar(off)) {
case '\t':
int tabIncr = (tabWidth - count) % tabWidth;
count += tabWidth - tabIncr;
break;
case (char) -1:
break;
default:
count++;
break;
}
lastOff++;
if (count == column) {
break;
} else if (count > column) {
lastOff--;
count = prevCount;
break;
}
}
// require region to include the necessary spacing
if (force && count < column && column != Integer.MAX_VALUE) {
lastOff += insertSpaces(document, count, lastOff, column - count, false, force).length();
count = column;
}
} catch (BadLocationException e) {
count = 0;
}
return new Region(lastOff, count);
}
}