| /******************************************************************************* |
| * Copyright (c) 2000, 2005 IBM Corporation and others. |
| * 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 |
| * |
| * 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()); |
| } |
| StringBuffer buf= new StringBuffer(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) { |
| } |
| return false; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand) |
| */ |
| 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); |
| } |
| } |