blob: 12fe616bde5e0a82ac631ce4ec3ccfd1b5c52b67 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2013 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ant.internal.ui.editor;
import org.eclipse.ant.internal.ui.AntUIPlugin;
import org.eclipse.ant.internal.ui.editor.formatter.XmlDocumentFormatter;
import org.eclipse.ant.internal.ui.model.AntElementNode;
import org.eclipse.ant.internal.ui.model.AntModel;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
/**
* Auto edit strategy for Ant build files Current does special indenting.
*/
public class AntAutoEditStrategy extends DefaultIndentLineAutoEditStrategy {
private AntModel fModel;
private int fAccumulatedChange = 0;
public AntAutoEditStrategy(AntModel model) {
fModel = model;
}
/**
* Sets the indentation based on the Ant element node that contains the offset of the document command.
*
* @param d
* the document to work on
* @param c
* the command to deal with
*/
private synchronized void autoIndentAfterNewLine(IDocument d, DocumentCommand c) {
if (c.offset == -1 || d.getLength() == 0 || fModel.getProjectNode(false) == null) {
return;
}
int position = (c.offset == d.getLength() ? c.offset - 1 : c.offset);
AntElementNode node = fModel.getProjectNode(false).getNode(position - fAccumulatedChange);
if (node == null) {
return;
}
try {
StringBuffer correct = XmlDocumentFormatter.getLeadingWhitespace(node.getOffset(), d);
if (!nextNodeIsEndTag(c.offset, d)) {
correct.append(XmlDocumentFormatter.createIndent());
}
StringBuilder buf = new StringBuilder(c.text);
buf.append(correct);
fAccumulatedChange += buf.length();
int line = d.getLineOfOffset(position);
IRegion reg = d.getLineInformation(line);
int lineEnd = reg.getOffset() + reg.getLength();
int contentStart = findEndOfWhiteSpace(d, c.offset, lineEnd);
c.length = Math.max(contentStart - c.offset, 0);
c.caretOffset = c.offset + buf.length();
c.shiftsCaret = false;
c.text = buf.toString();
}
catch (BadLocationException e) {
AntUIPlugin.log(e);
}
}
private boolean nextNodeIsEndTag(int offset, IDocument document) {
if (offset + 1 > document.getLength()) {
return false;
}
try {
IRegion lineRegion = document.getLineInformationOfOffset(offset);
offset = findEndOfWhiteSpace(document, offset, lineRegion.getOffset() + lineRegion.getLength());
String nextChars = document.get(offset, 2).trim();
if ("</".equals(nextChars) || "/>".equals(nextChars)) { //$NON-NLS-1$ //$NON-NLS-2$
return true;
}
}
catch (BadLocationException e) {
// do nothing
}
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
* org.eclipse.jface.text.DocumentCommand)
*/
@Override
public void customizeDocumentCommand(IDocument d, DocumentCommand c) {
if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text)) {
autoIndentAfterNewLine(d, c);
} else if (c.text.length() > 1) {
smartPaste(d, c);
}
}
private boolean isLineDelimiter(IDocument document, String text) {
String[] delimiters = document.getLegalLineDelimiters();
if (delimiters != null)
return TextUtilities.equals(delimiters, text) > -1;
return false;
}
public synchronized void reconciled() {
fAccumulatedChange = 0;
}
private void smartPaste(IDocument document, DocumentCommand command) {
try {
if (command.offset == -1 || document.getLength() == 0 || fModel.getProjectNode(false) == null) {
return;
}
String origChange = command.text;
int position = (command.offset == document.getLength() ? command.offset - 1 : command.offset);
AntElementNode node = fModel.getProjectNode(false).getNode(position - fAccumulatedChange);
if (node == null) {
return;
}
// eat any WS before the insertion to the beginning of the line
int firstLine = 1; // don't format the first line if it has other content before it
IRegion line = document.getLineInformationOfOffset(command.offset);
String notSelected = document.get(line.getOffset(), command.offset - line.getOffset());
if (notSelected.trim().length() == 0) {
command.length += notSelected.length();
command.offset = line.getOffset();
firstLine = 0;
}
// handle the indentation computation inside a temporary document
Document temp = new Document(command.text);
// indent the first and second line
// compute the relative indentation difference from the second line
// (as the first might be partially selected) and use the value to
// indent all other lines.
boolean isIndentDetected = false;
StringBuffer addition = new StringBuffer();
int insertLength = 0;
int lines = temp.getNumberOfLines();
for (int l = firstLine; l < lines; l++) { // we don't change the number of lines while adding indents
IRegion r = temp.getLineInformation(l);
int lineOffset = r.getOffset();
int lineLength = r.getLength();
if (lineLength == 0) { // don't modify empty lines
continue;
}
if (!isIndentDetected) {
// indent the first pasted line
StringBuffer current = XmlDocumentFormatter.getLeadingWhitespace(lineOffset, temp);
StringBuffer correct = XmlDocumentFormatter.getLeadingWhitespace(node.getOffset(), document);
correct.append(XmlDocumentFormatter.createIndent());
insertLength = subtractIndent(correct, current, addition);
isIndentDetected = true;
}
// relatively indent all pasted lines
if (insertLength > 0) {
addIndent(temp, l, addition);
} else if (insertLength < 0) {
cutIndent(temp, l, -insertLength);
}
}
// modify the command
if (!origChange.equals(temp.get())) {
fAccumulatedChange += temp.getLength();
command.text = temp.get();
}
}
catch (BadLocationException e) {
AntUIPlugin.log(e);
}
}
/**
* Indents line <code>line</code> in <code>document</code> with <code>indent</code>. Leaves leading comment signs alone.
*
* @param document
* the document
* @param line
* the line
* @param indent
* the indentation to insert
* @throws BadLocationException
* on concurrent document modification
*/
private void addIndent(Document document, int line, CharSequence indent) throws BadLocationException {
IRegion region = document.getLineInformation(line);
int insert = region.getOffset();
// insert indent
document.replace(insert, 0, indent.toString());
}
/**
* Cuts the visual equivalent of <code>toDelete</code> characters out of the indentation of line <code>line</code> in <code>document</code>.
*
* @param document
* the document
* @param line
* the line
* @param toDelete
* the number of space equivalents to delete.
* @throws BadLocationException
* on concurrent document modification
*/
private void cutIndent(Document document, int line, int toDelete) throws BadLocationException {
IRegion region = document.getLineInformation(line);
int from = region.getOffset();
int endOffset = region.getOffset() + region.getLength();
int to = from;
while (toDelete > 0 && to < endOffset) {
char ch = document.getChar(to);
if (!Character.isWhitespace(ch))
break;
toDelete -= computeVisualLength(ch);
if (toDelete >= 0) {
to++;
} else {
break;
}
}
document.replace(from, to - from, null);
}
/**
* Returns the visual length of a given character taking into account the visual tabulator length.
*
* @param ch
* the character to measure
* @return the visual length of <code>ch</code>
*/
private int computeVisualLength(char ch) {
if (ch == '\t') {
return getVisualTabLengthPreference();
}
return 1;
}
/**
* Returns the visual length of a given <code>CharSequence</code> taking into account the visual tabulator length.
*
* @param seq
* the string to measure
* @return the visual length of <code>seq</code>
*/
private int computeVisualLength(CharSequence seq) {
int size = 0;
int tablen = getVisualTabLengthPreference();
for (int i = 0; i < seq.length(); i++) {
char ch = seq.charAt(i);
if (ch == '\t') {
size += tablen - size % tablen;
} else {
size++;
}
}
return size;
}
/**
* Computes the difference of two indentations and returns the difference in length of current and correct. If the return value is positive,
* <code>addition</code> is initialized with a substring of that length of <code>correct</code>.
*
* @param correct
* the correct indentation
* @param current
* the current indentation (might contain non-whitespace)
* @param difference
* a string buffer - if the return value is positive, it will be cleared and set to the substring of <code>current</code> of that
* length
* @return the difference in length of <code>correct</code> and <code>current</code>
*/
private int subtractIndent(CharSequence correct, CharSequence current, StringBuffer difference) {
int c1 = computeVisualLength(correct);
int c2 = computeVisualLength(current);
int diff = c1 - c2;
if (diff <= 0) {
return diff;
}
difference.setLength(0);
int len = 0, i = 0;
while (len < diff) {
char c = correct.charAt(i++);
difference.append(c);
len += computeVisualLength(c);
}
return diff;
}
/**
* The preference setting for the visual tabulator display.
*
* @return the number of spaces displayed for a tabulator in the editor
*/
private int getVisualTabLengthPreference() {
return AntUIPlugin.getDefault().getCombinedPreferenceStore().getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH);
}
}