/* * Copyright (c) 2012, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.ui.internal.text.dart; import com.google.dart.tools.ui.DartToolsPlugin; import com.google.dart.tools.ui.PreferenceConstants; import com.google.dart.tools.ui.internal.text.editor.CompilationUnitEditor; import com.google.dart.tools.ui.internal.text.functions.SmartBackspaceManager; import com.google.dart.tools.ui.internal.text.functions.SmartBackspaceManager.UndoSpec; import com.google.dart.tools.ui.text.DartPartitions; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IAutoEditStrategy; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.TextUtilities; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.texteditor.ITextEditorExtension2; import org.eclipse.ui.texteditor.ITextEditorExtension3; import java.util.Arrays; /** * Modifies <code>DocumentCommand</code>s inserting semicolons and opening braces to place them * smartly, i.e. moving them to the end of a line if that is what the user expects. * <p> * In practice, semicolons and braces (and the caret) are moved to the end of the line if they are * typed anywhere except for semicolons in a <code>for</code> statements definition. If the line * contains a semicolon or brace after the current caret position, the cursor is moved after it. * </p> * * @see org.eclipse.jface.text.DocumentCommand */ public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy { // NOTE: With current preference settings this class is unused. Don't test it. /** String representation of a semicolon. */ private static final String SEMICOLON = ";"; //$NON-NLS-1$ /** Char representation of a semicolon. */ private static final char SEMICHAR = ';'; /** String represenattion of a opening brace. */ private static final String BRACE = "{"; //$NON-NLS-1$ /** Char representation of a opening brace */ private static final char BRACECHAR = '{'; /** * Computes the next insert position of the given character in the current line. * * @param document the document we are working on * @param line the line where the change is being made * @param offset the position of the caret in the line when <code>character</code> was typed * @param character the character to look for * @param partitioning the document partitioning * @return the position where <code>character</code> should be inserted / replaced */ protected static int computeCharacterPosition(IDocument document, ITextSelection line, int offset, char character, String partitioning) { String text = line.getText(); if (text == null) { return 0; } int insertPos; if (character == BRACECHAR) { insertPos = computeArrayInitializationPos(document, line, offset, partitioning); if (insertPos == -1) { insertPos = computeAfterTryDoElse(document, line, offset); } if (insertPos == -1) { insertPos = computeAfterParenthesis(document, line, offset, partitioning); } } else if (character == SEMICHAR) { if (isForStatement(text, offset)) { insertPos = -1; // don't do anything in for statements, as semis are // vital part of these } else { int nextPartitionPos = nextPartitionOrLineEnd(document, line, offset, partitioning); insertPos = startOfWhitespaceBeforeOffset(text, nextPartitionPos); // if there is a semi present, return its location as alreadyPresent() // will take it out this way. if (insertPos > 0 && text.charAt(insertPos - 1) == character) { insertPos = insertPos - 1; } else if (insertPos > 0 && text.charAt(insertPos - 1) == '}') { int opening = scanBackward( document, insertPos - 1 + line.getOffset(), partitioning, -1, new char[] {'{'}); if (opening > -1 && opening < offset + line.getOffset()) { if (computeArrayInitializationPos( document, line, opening - line.getOffset(), partitioning) == -1) { insertPos = offset; } } } } } else { Assert.isTrue(false); return -1; } return insertPos; } /** * Computes an insert position for an opening brace if <code>offset</code> maps to a position in * <code>document</code> with a expression in parenthesis that will take a block after the closing * parenthesis. * * @param document the document being modified * @param line the current line under investigation * @param offset the offset of the caret position, relative to the line start. * @param partitioning the document partitioning * @return an insert position relative to the line start if <code>line</code> contains a * parenthesized expression that can be followed by a block, -1 otherwise */ private static int computeAfterParenthesis(IDocument document, ITextSelection line, int offset, String partitioning) { // find the opening parenthesis for every closing parenthesis on the current // line after offset // return the position behind the closing parenthesis if it looks like a // method declaration // or an expression for an if, while, for, catch statement int pos = offset + line.getOffset(); int length = line.getOffset() + line.getLength(); int scanTo = scanForward(document, pos, partitioning, length, '}'); if (scanTo == -1) { scanTo = length; } int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1; while (true) { int startScan = closingParen + 1; closingParen = scanForward(document, startScan, partitioning, scanTo, ')'); if (closingParen == -1) { break; } int openingParen = findOpeningParenMatch(document, closingParen, partitioning); // no way an expression at the beginning of the document can mean anything if (openingParen < 1) { break; } // only select insert positions for parenthesis currently embracing the // caret if (openingParen > pos) { continue; } if (looksLikeAnonymousClassDef(document, openingParen - 1, partitioning)) { return closingParen + 1 - line.getOffset(); } if (looksLikeIfWhileForCatch(document, openingParen - 1, partitioning)) { return closingParen + 1 - line.getOffset(); } if (looksLikeMethodDecl(document, openingParen - 1, partitioning)) { return closingParen + 1 - line.getOffset(); } } return -1; } /** * Computes an insert position for an opening brace if <code>offset</code> maps to a position in * <code>doc</code> involving a keyword taking a block after it. These are: <code>try</code>, * <code>do</code>, <code>synchronized</code>, <code>static</code>, <code>finally</code>, or * <code>else</code>. * * @param doc the document being modified * @param line the current line under investigation * @param offset the offset of the caret position, relative to the line start. * @return an insert position relative to the line start if <code>line</code> contains one of the * above keywords at or before <code>offset</code> , -1 otherwise */ private static int computeAfterTryDoElse(IDocument doc, ITextSelection line, int offset) { // search backward while WS, find 'try', 'do', 'else' in default partition int p = offset + line.getOffset(); p = firstWhitespaceToRight(doc, p); if (p == -1) { return -1; } p--; if (looksLike(doc, p, "try") //$NON-NLS-1$ || looksLike(doc, p, "do") //$NON-NLS-1$ || looksLike(doc, p, "static") //$NON-NLS-1$ || looksLike(doc, p, "else")) { return p + 1 - line.getOffset(); } return -1; } /** * Computes an insert position for an opening brace if <code>offset</code> maps to a position in * <code>document</code> that looks like being the RHS of an assignment or like an array * definition. * * @param document the document being modified * @param line the current line under investigation * @param offset the offset of the caret position, relative to the line start. * @param partitioning the document partitioning * @return an insert position relative to the line start if <code>line</code> looks like being an * array initialization at <code>offset</code>, -1 otherwise */ private static int computeArrayInitializationPos(IDocument document, ITextSelection line, int offset, String partitioning) { // search backward while WS, find = (not != <= >= ==) in default partition int pos = offset + line.getOffset(); if (pos == 0) { return -1; } int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1); if (p == -1) { return -1; } try { char ch = document.getChar(p); if (ch != '=' && ch != ']') { return -1; } if (p == 0) { return offset; } p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1); if (p == -1) { return -1; } ch = document.getChar(p); if (Character.isJavaIdentifierPart(ch) || ch == ']' || ch == '[') { return offset; } } catch (BadLocationException e) { } return -1; } /** * From <code>position</code> to the left, eats any whitespace and then a pair of brackets as used * to declare an array return type like * * <pre> * String [ ] * </pre> * * . The return value is either the position of the opening bracket or <code>position</code> if no * pair of brackets can be parsed. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @return the smallest character position of bracket pair or <code>position</code> */ private static int eatBrackets(IDocument document, int position, String partitioning) { // accept array return type int pos = firstNonWhitespaceBackward(document, position, partitioning, -1); try { if (pos > 1 && document.getChar(pos) == ']') { pos = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1); if (pos > 0 && document.getChar(pos) == '[') { return pos; } } } catch (BadLocationException e) { // won't happen } return position; } /** * From <code>position</code> to the left, eats any whitespace and the first identifier, returning * the position of the first identifier character (in normal read order). * <p> * When called on a document with content <code>" some string "</code> and positionition 13, the * return value will be 6 (the first letter in <code>string</code>). * </p> * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @return the smallest character position of an identifier or -1 if none can be found; always * <= <code>position</code> */ private static int eatIdentToLeft(IDocument document, int position, String partitioning) { if (position < 0) { return -1; } Assert.isTrue(position < document.getLength()); int p = firstNonWhitespaceBackward(document, position, partitioning, -1); if (p == -1) { return -1; } try { while (p >= 0) { char ch = document.getChar(p); if (Character.isJavaIdentifierPart(ch)) { p--; continue; } // length must be > 0 if (Character.isWhitespace(ch) && p != position) { return p + 1; } else { return -1; } } // start of document reached return 0; } catch (BadLocationException e) { } return -1; } /** * Finds a closing parenthesis to the left of <code>position</code> in document, where that * parenthesis is only separated by whitespace from <code>position</code>. If no such parenthesis * can be found, <code>position</code> is returned. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @return the position of a closing parenthesis left to <code>position</code> separated only by * whitespace, or <code>position</code> if no parenthesis can be found */ private static int findClosingParenToLeft(IDocument document, int position, String partitioning) { final char CLOSING_PAREN = ')'; try { if (position < 1) { return position; } int nonWS = firstNonWhitespaceBackward(document, position - 1, partitioning, -1); if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN) { return nonWS; } } catch (BadLocationException e1) { } return position; } /** * Finds the position of the parenthesis matching the closing parenthesis at <code>position</code> * . * * @param document the document being modified * @param position the position in <code>document</code> of a closing parenthesis * @param partitioning the document partitioning * @return the position in <code>document</code> of the matching parenthesis, or -1 if none can be * found */ private static int findOpeningParenMatch(IDocument document, int position, String partitioning) { final char CLOSING_PAREN = ')'; final char OPENING_PAREN = '('; Assert.isTrue(position < document.getLength()); Assert.isTrue(position >= 0); Assert.isTrue(isDefaultPartition(document, position, partitioning)); try { Assert.isTrue(document.getChar(position) == CLOSING_PAREN); int depth = 1; while (true) { position = scanBackward(document, position - 1, partitioning, -1, new char[] { CLOSING_PAREN, OPENING_PAREN}); if (position == -1) { return -1; } if (document.getChar(position) == CLOSING_PAREN) { depth++; } else { depth--; } if (depth == 0) { return position; } } } catch (BadLocationException e) { return -1; } } /** * Finds the highest position in <code>document</code> such that the position is <= * <code>position</code> and > <code>bound</code> and * <code>Character.isWhitespace(document.getChar(pos))</code> evaluates to <code>false</code> and * the position is in the default partition. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @param bound the first position in <code>document</code> to not consider any more, with * <code>bound</code> < <code>position</code> * @return the highest position of one element in <code>chars</code> in [ <code>position</code>, * <code>scanTo</code>) that resides in a Java partition, or <code>-1</code> if none can * be found */ private static int firstNonWhitespaceBackward(IDocument document, int position, String partitioning, int bound) { Assert.isTrue(position < document.getLength()); Assert.isTrue(bound >= -1); try { while (position > bound) { char ch = document.getChar(position); if (!Character.isWhitespace(ch) && isDefaultPartition(document, position, partitioning)) { return position; } position--; } } catch (BadLocationException e) { } return -1; } /** * Finds the smallest position in <code>document</code> such that the position is >= * <code>position</code> and < <code>bound</code> and * <code>Character.isWhitespace(document.getChar(pos))</code> evaluates to <code>false</code> and * the position is in the default partition. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @param bound the first position in <code>document</code> to not consider any more, with * <code>bound</code> > <code>position</code> * @return the smallest position of one element in <code>chars</code> in [ <code>position</code>, * <code>scanTo</code>) that resides in a Java partition, or <code>-1</code> if none can * be found */ private static int firstNonWhitespaceForward(IDocument document, int position, String partitioning, int bound) { Assert.isTrue(position >= 0); Assert.isTrue(bound <= document.getLength()); try { while (position < bound) { char ch = document.getChar(position); if (!Character.isWhitespace(ch) && isDefaultPartition(document, position, partitioning)) { return position; } position++; } } catch (BadLocationException e) { } return -1; } /** * Finds the first whitespace character position to the right of (and including) * <code>position</code>. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @return the position of a whitespace character greater or equal than <code>position</code> * separated only by whitespace, or -1 if none found */ private static int firstWhitespaceToRight(IDocument document, int position) { int length = document.getLength(); Assert.isTrue(position >= 0); Assert.isTrue(position <= length); try { while (position < length) { char ch = document.getChar(position); if (Character.isWhitespace(ch)) { return position; } position++; } return position; } catch (BadLocationException e) { } return -1; } /** * Returns a valid insert location (except for whitespace) in <code>partition</code> or -1 if * there is no valid insert location. An valid insert location is right after any java string or * character partition, or at the end of a java default partition, but never behind * <code>maxOffset</code>. Comment partitions or empty java partitions do never yield valid insert * positions. * * @param doc the document being modified * @param partition the current partition * @param maxOffset the maximum offset to consider * @return a valid insert location in <code>partition</code>, or -1 if there is no valid insert * location */ private static int getValidPositionForPartition(IDocument doc, ITypedRegion partition, int maxOffset) { final int INVALID = -1; if (DartPartitions.DART_DOC.equals(partition.getType())) { return INVALID; } if (DartPartitions.DART_MULTI_LINE_COMMENT.equals(partition.getType())) { return INVALID; } if (DartPartitions.DART_SINGLE_LINE_COMMENT.equals(partition.getType())) { return INVALID; } if (DartPartitions.DART_SINGLE_LINE_DOC.equals(partition.getType())) { return INVALID; } int endOffset = Math.min(maxOffset, partition.getOffset() + partition.getLength()); if (DartPartitions.DART_MULTI_LINE_STRING.equals(partition.getType())) { return endOffset; } if (DartPartitions.DART_STRING.equals(partition.getType())) { return endOffset; } if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) { try { if (doc.get(partition.getOffset(), endOffset - partition.getOffset()).trim().length() == 0) { return INVALID; } else { return endOffset; } } catch (BadLocationException e) { return INVALID; } } // default: we don't know anything about the partition - assume valid return endOffset; } /** * Checks whether <code>position</code> resides in a default (Java) partition of * <code>document</code>. * * @param document the document being modified * @param position the position to be checked * @param partitioning the document partitioning * @return <code>true</code> if <code>position</code> is in the default partition of * <code>document</code>, <code>false</code> otherwise */ private static boolean isDefaultPartition(IDocument document, int position, String partitioning) { Assert.isTrue(position >= 0); Assert.isTrue(position <= document.getLength()); try { // don't use getPartition2 since we're interested in the scanned // character's partition ITypedRegion region = TextUtilities.getPartition(document, partitioning, position, false); return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE); } catch (BadLocationException e) { } return false; } /** * Determines whether the current line contains a for statement. Algorithm: any "for" word in the * line is a positive, "for" contained in a string literal will produce a false positive. * * @param line the line where the change is being made * @param offset the position of the caret * @return <code>true</code> if <code>line</code> contains <code>for</code>, <code>false</code> * otherwise */ private static boolean isForStatement(String line, int offset) { /* searching for (^|\s)for(\s|$) */ int forPos = line.indexOf("for"); //$NON-NLS-1$ if (forPos != -1) { if ((forPos == 0 || !Character.isJavaIdentifierPart(line.charAt(forPos - 1))) && (line.length() == forPos + 3 || !Character.isJavaIdentifierPart(line.charAt(forPos + 3)))) { return true; } } return false; } /** * Checks whether the content of <code>document</code> in the range ( <code>offset</code>, * <code>length</code>) contains the <code>new</code> keyword. * * @param document the document being modified * @param offset the first character position in <code>document</code> to be considered * @param length the length of the character range to be considered * @param partitioning the document partitioning * @return <code>true</code> if the specified character range contains a <code>new</code> keyword, * <code>false</code> otherwise. */ private static boolean isNewMatch(IDocument document, int offset, int length, String partitioning) { Assert.isTrue(length >= 0); Assert.isTrue(offset >= 0); Assert.isTrue(offset + length < document.getLength() + 1); try { String text = document.get(offset, length); int pos = text.indexOf("new"); //$NON-NLS-1$ while (pos != -1 && !isDefaultPartition(document, pos + offset, partitioning)) { pos = text.indexOf("new", pos + 2); //$NON-NLS-1$ } if (pos < 0) { return false; } if (pos != 0 && Character.isJavaIdentifierPart(text.charAt(pos - 1))) { return false; } if (pos + 3 < length && Character.isJavaIdentifierPart(text.charAt(pos + 3))) { return false; } return true; } catch (BadLocationException e) { } return false; } /** * Checks whether code>document</code> contains the <code>String</code> <code>like</code> such * that its last character is at <code>position</code>. If <code>like</code> starts with a * identifier part (as determined by {@link Character#isJavaIdentifierPart(char)}), it is also * made sure that <code>like</code> is preceded by some non-identifier character or stands at the * document start. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param like the <code>String</code> to look for. * @return <code>true</code> if <code>document</code> contains <code>like</code> such that it ends * at <code>position</code>, <code>false</code> otherwise */ private static boolean looksLike(IDocument document, int position, String like) { int length = like.length(); if (position < length - 1) { return false; } try { if (!like.equals(document.get(position - length + 1, length))) { return false; } if (position >= length && Character.isJavaIdentifierPart(like.charAt(0)) && Character.isJavaIdentifierPart(document.getChar(position - length))) { return false; } } catch (BadLocationException e) { return false; } return true; } /** * Checks whether the content of <code>document</code> at <code>position</code> looks like an * anonymous class definition. <code>position</code> must be to the left of the opening * parenthesis of the definition's parameter list. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @return <code>true</code> if the content of <code>document</code> looks like an anonymous class * definition, <code>false</code> otherwise */ private static boolean looksLikeAnonymousClassDef(IDocument document, int position, String partitioning) { int previousCommaParenEqual = scanBackward( document, position - 1, partitioning, -1, new char[] {',', '(', '='}); if (previousCommaParenEqual == -1 || position < previousCommaParenEqual + 5) { // for borders, 3 for "new" return false; } if (isNewMatch( document, previousCommaParenEqual + 1, position - previousCommaParenEqual - 2, partitioning)) { return true; } return false; } /** * Checks whether, to the left of <code>position</code> and separated only by whitespace, * <code>document</code> contains a keyword taking a parameter list and a block after it. These * are: <code>if</code>, <code>while</code>, <code>catch</code>, <code>for</code>, * <code>synchronized</code>, <code>switch</code>. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @return <code>true</code> if <code>document</code> contains any of the above keywords to the * left of <code>position</code>, <code>false</code> otherwise */ private static boolean looksLikeIfWhileForCatch(IDocument document, int position, String partitioning) { position = firstNonWhitespaceBackward(document, position, partitioning, -1); if (position == -1) { return false; } return looksLike(document, position, "if") //$NON-NLS-1$ || looksLike(document, position, "while") //$NON-NLS-1$ || looksLike(document, position, "catch") //$NON-NLS-1$ || looksLike(document, position, "switch") //$NON-NLS-1$ || looksLike(document, position, "for"); //$NON-NLS-1$ } /** * Checks whether the content of <code>document</code> at <code>position</code> looks like a * method declaration header (i.e. only the return type and method name). <code>position</code> * must be just left of the opening parenthesis of the parameter list. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @return <code>true</code> if the content of <code>document</code> looks like a method * definition, <code>false</code> otherwise */ private static boolean looksLikeMethodDecl(IDocument document, int position, String partitioning) { // method name position = eatIdentToLeft(document, position, partitioning); if (position < 1) { return false; } position = eatBrackets(document, position - 1, partitioning); if (position < 1) { return false; } position = eatIdentToLeft(document, position - 1, partitioning); return position != -1; } /** * Returns a position in the first java partition after the last non-empty and non-comment * partition. There is no non-whitespace from the returned position to the end of the partition it * is contained in. * * @param document the document being modified * @param line the line under investigation * @param offset the caret offset into <code>line</code> * @param partitioning the document partitioning * @return the position of the next Java partition, or the end of <code>line</code> */ private static int nextPartitionOrLineEnd(IDocument document, ITextSelection line, int offset, String partitioning) { // run relative to document final int docOffset = offset + line.getOffset(); final int eol = line.getOffset() + line.getLength(); int nextPartitionPos = eol; // init with line end int validPosition = docOffset; try { ITypedRegion partition = TextUtilities.getPartition( document, partitioning, nextPartitionPos, true); validPosition = getValidPositionForPartition(document, partition, eol); while (validPosition == -1) { nextPartitionPos = partition.getOffset() - 1; if (nextPartitionPos < docOffset) { validPosition = docOffset; break; } partition = TextUtilities.getPartition(document, partitioning, nextPartitionPos, false); validPosition = getValidPositionForPartition(document, partition, eol); } } catch (BadLocationException e) { } validPosition = Math.max(validPosition, docOffset); // make relative to line validPosition -= line.getOffset(); return validPosition; } /** * Finds the highest position in <code>document</code> such that the position is <= * <code>position</code> and > <code>bound</code> and * <code>document.getChar(position) == ch</code> evaluates to <code>true</code> for at least one * ch in <code>chars</code> and the position is in the default partition. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @param bound the first position in <code>document</code> to not consider any more, with * <code>scanTo</code> > <code>position</code> * @param chars an array of <code>char</code> to search for * @return the highest position of one element in <code>chars</code> in ( <code>bound</code>, * <code>position</code>] that resides in a Java partition, or <code>-1</code> if none can * be found */ private static int scanBackward(IDocument document, int position, String partitioning, int bound, char[] chars) { Assert.isTrue(bound >= -1); Assert.isTrue(position < document.getLength()); Arrays.sort(chars); try { while (position > bound) { if (Arrays.binarySearch(chars, document.getChar(position)) >= 0 && isDefaultPartition(document, position, partitioning)) { return position; } position--; } } catch (BadLocationException e) { } return -1; } /** * Finds the lowest position in <code>document</code> such that the position is >= * <code>position</code> and < <code>bound</code> and * <code>document.getChar(position) == ch</code> evaluates to <code>true</code> and the position * is in the default partition. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @param bound the first position in <code>document</code> to not consider any more, with * <code>scanTo</code> > <code>position</code> * @param ch a <code>char</code> to search for * @return the lowest position of one element in <code>chars</code> in [ <code>position</code>, * <code>bound</code>) that resides in a Java partition, or <code>-1</code> if none can be * found */ private static int scanForward(IDocument document, int position, String partitioning, int bound, char ch) { return scanForward(document, position, partitioning, bound, new char[] {ch}); } // /** // * Finds the highest position in <code>document</code> such that the position is <= <code>position</code> // * and > <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code> // * and the position is in the default partition. // * // * @param document the document being modified // * @param position the first character position in <code>document</code> to be considered // * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code> // * @param chars an array of <code>char</code> to search for // * @return the highest position of one element in <code>chars</code> in [<code>position</code>, <code>scanTo</code>) that resides in a Java partition, or <code>-1</code> if none can be found // */ // private static int scanBackward(IDocument document, int position, int bound, char ch) { // return scanBackward(document, position, bound, new char[] {ch}); // } // /** * Finds the lowest position in <code>document</code> such that the position is >= * <code>position</code> and < <code>bound</code> and * <code>document.getChar(position) == ch</code> evaluates to <code>true</code> for at least one * ch in <code>chars</code> and the position is in the default partition. * * @param document the document being modified * @param position the first character position in <code>document</code> to be considered * @param partitioning the document partitioning * @param bound the first position in <code>document</code> to not consider any more, with * <code>scanTo</code> > <code>position</code> * @param chars an array of <code>char</code> to search for * @return the lowest position of one element in <code>chars</code> in [ <code>position</code>, * <code>bound</code>) that resides in a Java partition, or <code>-1</code> if none can be * found */ private static int scanForward(IDocument document, int position, String partitioning, int bound, char[] chars) { Assert.isTrue(position >= 0); Assert.isTrue(bound <= document.getLength()); Arrays.sort(chars); try { while (position < bound) { if (Arrays.binarySearch(chars, document.getChar(position)) >= 0 && isDefaultPartition(document, position, partitioning)) { return position; } position++; } } catch (BadLocationException e) { } return -1; } /** * Returns the position in <code>text</code> after which there comes only whitespace, up to * <code>offset</code>. * * @param text the text being searched * @param offset the maximum offset to search for * @return the smallest value <code>v</code> such that * <code>text.substring(v, offset).trim() == 0</code> */ private static int startOfWhitespaceBeforeOffset(String text, int offset) { int i = Math.min(offset, text.length()); for (; i >= 1; i--) { if (!Character.isWhitespace(text.charAt(i - 1))) { break; } } return i; } private char fCharacter; private String fPartitioning; /** * Creates a new SmartSemicolonAutoEditStrategy. * * @param partitioning the document partitioning */ public SmartSemicolonAutoEditStrategy(String partitioning) { fPartitioning = partitioning; } /* * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse * .jface.text.IDocument, org.eclipse.jface.text.DocumentCommand) */ @Override public void customizeDocumentCommand(IDocument document, DocumentCommand command) { // 0: early pruning // also customize if <code>doit</code> is false (so it works in code // completion situations) // if (!command.doit) // return; if (command.text == null) { return; } if (command.text.equals(SEMICOLON)) { fCharacter = SEMICHAR; } else if (command.text.equals(BRACE)) { fCharacter = BRACECHAR; } else { return; } IPreferenceStore store = DartToolsPlugin.getDefault().getPreferenceStore(); if (fCharacter == SEMICHAR && !store.getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON)) { return; } if (fCharacter == BRACECHAR && !store.getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE)) { return; } IWorkbenchPage page = DartToolsPlugin.getActivePage(); if (page == null) { return; } IEditorPart part = page.getActiveEditor(); if (!(part instanceof CompilationUnitEditor)) { return; } CompilationUnitEditor editor = (CompilationUnitEditor) part; if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT || !editor.isEditable()) { return; } ITextEditorExtension2 extension = (ITextEditorExtension2) editor.getAdapter(ITextEditorExtension2.class); if (extension != null && !extension.validateEditorInputState()) { return; } if (isMultilineSelection(document, command)) { return; } // 1: find concerned line / position in java code, location in statement int pos = command.offset; ITextSelection line; try { IRegion l = document.getLineInformationOfOffset(pos); line = new TextSelection(document, l.getOffset(), l.getLength()); } catch (BadLocationException e) { return; } // 2: choose action based on findings (is for-Statement?) // for now: compute the best position to insert the new character int positionInLine = computeCharacterPosition( document, line, pos - line.getOffset(), fCharacter, fPartitioning); int position = positionInLine + line.getOffset(); // never position before the current position! if (position < pos) { return; } // never double already existing content if (alreadyPresent(document, fCharacter, position)) { return; } // don't do special processing if what we do is actually the normal // behaviour String insertion = adjustSpacing(document, position, fCharacter); if (command.offset == position && insertion.equals(command.text)) { return; } try { final SmartBackspaceManager manager = (SmartBackspaceManager) editor.getAdapter(SmartBackspaceManager.class); if (manager != null && DartToolsPlugin.getDefault().getPreferenceStore().getBoolean( PreferenceConstants.EDITOR_SMART_BACKSPACE)) { TextEdit e1 = new ReplaceEdit(command.offset, command.text.length(), document.get( command.offset, command.length)); UndoSpec s1 = new UndoSpec(command.offset + command.text.length(), new Region( command.offset, 0), new TextEdit[] {e1}, 0, null); DeleteEdit smart = new DeleteEdit(position, insertion.length()); ReplaceEdit raw = new ReplaceEdit(command.offset, command.length, command.text); UndoSpec s2 = new UndoSpec(position + insertion.length(), new Region(command.offset + command.text.length(), 0), new TextEdit[] {smart, raw}, 2, s1); manager.register(s2); } // 3: modify command command.offset = position; command.length = 0; command.caretOffset = position; command.text = insertion; command.doit = true; command.owner = null; } catch (MalformedTreeException e) { DartToolsPlugin.log(e); } catch (BadLocationException e) { DartToolsPlugin.log(e); } } /** * Adds a space before a brace if it is inserted after a parenthesis, equal sign, or one of the * keywords <code>try, else, do</code>. * * @param doc the document we are working on * @param position the insert position of <code>character</code> * @param character the character to be inserted * @return a <code>String</code> consisting of <code>character</code> plus any additional spacing */ private String adjustSpacing(IDocument doc, int position, char character) { if (character == BRACECHAR) { if (position > 0 && position <= doc.getLength()) { int pos = position - 1; if (looksLike(doc, pos, ")") //$NON-NLS-1$ || looksLike(doc, pos, "=") //$NON-NLS-1$ || looksLike(doc, pos, "]") //$NON-NLS-1$ || looksLike(doc, pos, "try") //$NON-NLS-1$ || looksLike(doc, pos, "else") //$NON-NLS-1$ || looksLike(doc, pos, "static") //$NON-NLS-1$ || looksLike(doc, pos, "do")) { return new String(new char[] {' ', character}); } } } return new String(new char[] {character}); } /** * Checks whether a character to be inserted is already present at the insert location (perhaps * separated by some whitespace from <code>position</code>. * * @param document the document we are working on * @param position the insert position of <code>ch</code> * @param ch the character to be inserted * @return <code>true</code> if <code>ch</code> is already present at <code>location</code>, * <code>false</code> otherwise */ private boolean alreadyPresent(IDocument document, char ch, int position) { int pos = firstNonWhitespaceForward(document, position, fPartitioning, document.getLength()); try { if (pos != -1 && document.getChar(pos) == ch) { return true; } } catch (BadLocationException e) { } return false; } /** * Returns <code>true</code> if the document command is applied on a multi line selection, * <code>false</code> otherwise. * * @param document the document * @param command the command * @return <code>true</code> if <code>command</code> is a multiline command */ private boolean isMultilineSelection(IDocument document, DocumentCommand command) { try { return document.getNumberOfLines(command.offset, command.length) > 1; } catch (BadLocationException e) { // ignore return false; } } }