| /******************************************************************************* |
| * 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; |
| } |
| |
| @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); |
| } |
| } |