0001: /*******************************************************************************
0002: * Copyright (c) 2000, 2006 IBM Corporation and others.
0003: * All rights reserved. This program and the accompanying materials
0004: * are made available under the terms of the Eclipse Public License v1.0
0005: * which accompanies this distribution, and is available at
0006: * http://www.eclipse.org/legal/epl-v10.html
0007: *
0008: * Contributors:
0009: * IBM Corporation - initial API and implementation
0010: *******************************************************************************/package org.eclipse.jdt.internal.ui.text.java;
0011:
0012: import java.util.Arrays;
0013:
0014: import org.eclipse.text.edits.DeleteEdit;
0015: import org.eclipse.text.edits.MalformedTreeException;
0016: import org.eclipse.text.edits.ReplaceEdit;
0017: import org.eclipse.text.edits.TextEdit;
0018:
0019: import org.eclipse.core.runtime.Assert;
0020:
0021: import org.eclipse.jface.preference.IPreferenceStore;
0022:
0023: import org.eclipse.jface.text.BadLocationException;
0024: import org.eclipse.jface.text.DocumentCommand;
0025: import org.eclipse.jface.text.IAutoEditStrategy;
0026: import org.eclipse.jface.text.IDocument;
0027: import org.eclipse.jface.text.IRegion;
0028: import org.eclipse.jface.text.ITextSelection;
0029: import org.eclipse.jface.text.ITypedRegion;
0030: import org.eclipse.jface.text.Region;
0031: import org.eclipse.jface.text.TextSelection;
0032: import org.eclipse.jface.text.TextUtilities;
0033:
0034: import org.eclipse.ui.IEditorPart;
0035: import org.eclipse.ui.IWorkbenchPage;
0036: import org.eclipse.ui.texteditor.ITextEditorExtension2;
0037: import org.eclipse.ui.texteditor.ITextEditorExtension3;
0038:
0039: import org.eclipse.jdt.ui.PreferenceConstants;
0040: import org.eclipse.jdt.ui.text.IJavaPartitions;
0041:
0042: import org.eclipse.jdt.internal.ui.JavaPlugin;
0043: import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
0044: import org.eclipse.jdt.internal.ui.text.SmartBackspaceManager;
0045: import org.eclipse.jdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
0046:
0047: /**
0048: * Modifies <code>DocumentCommand</code>s inserting semicolons and opening braces to place them
0049: * smartly, i.e. moving them to the end of a line if that is what the user expects.
0050: *
0051: * <p>In practice, semicolons and braces (and the caret) are moved to the end of the line if they are typed
0052: * anywhere except for semicolons in a <code>for</code> statements definition. If the line contains a semicolon
0053: * or brace after the current caret position, the cursor is moved after it.</p>
0054: *
0055: * @see org.eclipse.jface.text.DocumentCommand
0056: * @since 3.0
0057: */
0058: public class SmartSemicolonAutoEditStrategy implements
0059: IAutoEditStrategy {
0060:
0061: /** String representation of a semicolon. */
0062: private static final String SEMICOLON = ";"; //$NON-NLS-1$
0063: /** Char representation of a semicolon. */
0064: private static final char SEMICHAR = ';';
0065: /** String represenattion of a opening brace. */
0066: private static final String BRACE = "{"; //$NON-NLS-1$
0067: /** Char representation of a opening brace */
0068: private static final char BRACECHAR = '{';
0069:
0070: private char fCharacter;
0071: private String fPartitioning;
0072:
0073: /**
0074: * Creates a new SmartSemicolonAutoEditStrategy.
0075: *
0076: * @param partitioning the document partitioning
0077: */
0078: public SmartSemicolonAutoEditStrategy(String partitioning) {
0079: fPartitioning = partitioning;
0080: }
0081:
0082: /*
0083: * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
0084: */
0085: public void customizeDocumentCommand(IDocument document,
0086: DocumentCommand command) {
0087: // 0: early pruning
0088: // also customize if <code>doit</code> is false (so it works in code completion situations)
0089: // if (!command.doit)
0090: // return;
0091:
0092: if (command.text == null)
0093: return;
0094:
0095: if (command.text.equals(SEMICOLON))
0096: fCharacter = SEMICHAR;
0097: else if (command.text.equals(BRACE))
0098: fCharacter = BRACECHAR;
0099: else
0100: return;
0101:
0102: IPreferenceStore store = JavaPlugin.getDefault()
0103: .getPreferenceStore();
0104: if (fCharacter == SEMICHAR
0105: && !store
0106: .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
0107: return;
0108: if (fCharacter == BRACECHAR
0109: && !store
0110: .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
0111: return;
0112:
0113: IWorkbenchPage page = JavaPlugin.getActivePage();
0114: if (page == null)
0115: return;
0116: IEditorPart part = page.getActiveEditor();
0117: if (!(part instanceof CompilationUnitEditor))
0118: return;
0119: CompilationUnitEditor editor = (CompilationUnitEditor) part;
0120: if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
0121: || !editor.isEditable())
0122: return;
0123: ITextEditorExtension2 extension = (ITextEditorExtension2) editor
0124: .getAdapter(ITextEditorExtension2.class);
0125: if (extension != null && !extension.validateEditorInputState())
0126: return;
0127: if (isMultilineSelection(document, command))
0128: return;
0129:
0130: // 1: find concerned line / position in java code, location in statement
0131: int pos = command.offset;
0132: ITextSelection line;
0133: try {
0134: IRegion l = document.getLineInformationOfOffset(pos);
0135: line = new TextSelection(document, l.getOffset(), l
0136: .getLength());
0137: } catch (BadLocationException e) {
0138: return;
0139: }
0140:
0141: // 2: choose action based on findings (is for-Statement?)
0142: // for now: compute the best position to insert the new character
0143: int positionInLine = computeCharacterPosition(document, line,
0144: pos - line.getOffset(), fCharacter, fPartitioning);
0145: int position = positionInLine + line.getOffset();
0146:
0147: // never position before the current position!
0148: if (position < pos)
0149: return;
0150:
0151: // never double already existing content
0152: if (alreadyPresent(document, fCharacter, position))
0153: return;
0154:
0155: // don't do special processing if what we do is actually the normal behaviour
0156: String insertion = adjustSpacing(document, position, fCharacter);
0157: if (command.offset == position
0158: && insertion.equals(command.text))
0159: return;
0160:
0161: try {
0162:
0163: final SmartBackspaceManager manager = (SmartBackspaceManager) editor
0164: .getAdapter(SmartBackspaceManager.class);
0165: if (manager != null
0166: && JavaPlugin
0167: .getDefault()
0168: .getPreferenceStore()
0169: .getBoolean(
0170: PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
0171: TextEdit e1 = new ReplaceEdit(command.offset,
0172: command.text.length(), document.get(
0173: command.offset, command.length));
0174: UndoSpec s1 = new UndoSpec(command.offset
0175: + command.text.length(), new Region(
0176: command.offset, 0), new TextEdit[] { e1 }, 0,
0177: null);
0178:
0179: DeleteEdit smart = new DeleteEdit(position, insertion
0180: .length());
0181: ReplaceEdit raw = new ReplaceEdit(command.offset,
0182: command.length, command.text);
0183: UndoSpec s2 = new UndoSpec(position
0184: + insertion.length(), new Region(command.offset
0185: + command.text.length(), 0), new TextEdit[] {
0186: smart, raw }, 2, s1);
0187: manager.register(s2);
0188: }
0189:
0190: // 3: modify command
0191: command.offset = position;
0192: command.length = 0;
0193: command.caretOffset = position;
0194: command.text = insertion;
0195: command.doit = true;
0196: command.owner = null;
0197: } catch (MalformedTreeException e) {
0198: JavaPlugin.log(e);
0199: } catch (BadLocationException e) {
0200: JavaPlugin.log(e);
0201: }
0202:
0203: }
0204:
0205: /**
0206: * Returns <code>true</code> if the document command is applied on a multi
0207: * line selection, <code>false</code> otherwise.
0208: *
0209: * @param document the document
0210: * @param command the command
0211: * @return <code>true</code> if <code>command</code> is a multiline command
0212: */
0213: private boolean isMultilineSelection(IDocument document,
0214: DocumentCommand command) {
0215: try {
0216: return document.getNumberOfLines(command.offset,
0217: command.length) > 1;
0218: } catch (BadLocationException e) {
0219: // ignore
0220: return false;
0221: }
0222: }
0223:
0224: /**
0225: * Adds a space before a brace if it is inserted after a parenthesis, equal sign, or one
0226: * of the keywords <code>try, else, do</code>.
0227: *
0228: * @param doc the document we are working on
0229: * @param position the insert position of <code>character</code>
0230: * @param character the character to be inserted
0231: * @return a <code>String</code> consisting of <code>character</code> plus any additional spacing
0232: */
0233: private String adjustSpacing(IDocument doc, int position,
0234: char character) {
0235: if (character == BRACECHAR) {
0236: if (position > 0 && position <= doc.getLength()) {
0237: int pos = position - 1;
0238: if (looksLike(doc, pos, ")") //$NON-NLS-1$
0239: || looksLike(doc, pos, "=") //$NON-NLS-1$
0240: || looksLike(doc, pos, "]") //$NON-NLS-1$
0241: || looksLike(doc, pos, "try") //$NON-NLS-1$
0242: || looksLike(doc, pos, "else") //$NON-NLS-1$
0243: || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
0244: || looksLike(doc, pos, "static") //$NON-NLS-1$
0245: || looksLike(doc, pos, "finally") //$NON-NLS-1$
0246: || looksLike(doc, pos, "do")) //$NON-NLS-1$
0247: return new String(new char[] { ' ', character });
0248: }
0249: }
0250:
0251: return new String(new char[] { character });
0252: }
0253:
0254: /**
0255: * Checks whether a character to be inserted is already present at the insert location (perhaps
0256: * separated by some whitespace from <code>position</code>.
0257: *
0258: * @param document the document we are working on
0259: * @param position the insert position of <code>ch</code>
0260: * @param ch the character to be inserted
0261: * @return <code>true</code> if <code>ch</code> is already present at <code>location</code>, <code>false</code> otherwise
0262: */
0263: private boolean alreadyPresent(IDocument document, char ch,
0264: int position) {
0265: int pos = firstNonWhitespaceForward(document, position,
0266: fPartitioning, document.getLength());
0267: try {
0268: if (pos != -1 && document.getChar(pos) == ch)
0269: return true;
0270: } catch (BadLocationException e) {
0271: }
0272:
0273: return false;
0274: }
0275:
0276: /**
0277: * Computes the next insert position of the given character in the current line.
0278: *
0279: * @param document the document we are working on
0280: * @param line the line where the change is being made
0281: * @param offset the position of the caret in the line when <code>character</code> was typed
0282: * @param character the character to look for
0283: * @param partitioning the document partitioning
0284: * @return the position where <code>character</code> should be inserted / replaced
0285: */
0286: protected static int computeCharacterPosition(IDocument document,
0287: ITextSelection line, int offset, char character,
0288: String partitioning) {
0289: String text = line.getText();
0290: if (text == null)
0291: return 0;
0292:
0293: int insertPos;
0294: if (character == BRACECHAR) {
0295:
0296: insertPos = computeArrayInitializationPos(document, line,
0297: offset, partitioning);
0298:
0299: if (insertPos == -1) {
0300: insertPos = computeAfterTryDoElse(document, line,
0301: offset);
0302: }
0303:
0304: if (insertPos == -1) {
0305: insertPos = computeAfterParenthesis(document, line,
0306: offset, partitioning);
0307: }
0308:
0309: } else if (character == SEMICHAR) {
0310:
0311: if (isForStatement(text, offset)) {
0312: insertPos = -1; // don't do anything in for statements, as semis are vital part of these
0313: } else {
0314: int nextPartitionPos = nextPartitionOrLineEnd(document,
0315: line, offset, partitioning);
0316: insertPos = startOfWhitespaceBeforeOffset(text,
0317: nextPartitionPos);
0318: // if there is a semi present, return its location as alreadyPresent() will take it out this way.
0319: if (insertPos > 0
0320: && text.charAt(insertPos - 1) == character)
0321: insertPos = insertPos - 1;
0322: else if (insertPos > 0
0323: && text.charAt(insertPos - 1) == '}') {
0324: int opening = scanBackward(document, insertPos - 1
0325: + line.getOffset(), partitioning, -1,
0326: new char[] { '{' });
0327: if (opening > -1
0328: && opening < offset + line.getOffset()) {
0329: if (computeArrayInitializationPos(document,
0330: line, opening - line.getOffset(),
0331: partitioning) == -1) {
0332: insertPos = offset;
0333: }
0334: }
0335: }
0336: }
0337:
0338: } else {
0339: Assert.isTrue(false);
0340: return -1;
0341: }
0342:
0343: return insertPos;
0344: }
0345:
0346: /**
0347: * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
0348: * <code>document</code> that looks like being the RHS of an assignment or like an array definition.
0349: *
0350: * @param document the document being modified
0351: * @param line the current line under investigation
0352: * @param offset the offset of the caret position, relative to the line start.
0353: * @param partitioning the document partitioning
0354: * @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
0355: */
0356: private static int computeArrayInitializationPos(
0357: IDocument document, ITextSelection line, int offset,
0358: String partitioning) {
0359: // search backward while WS, find = (not != <= >= ==) in default partition
0360: int pos = offset + line.getOffset();
0361:
0362: if (pos == 0)
0363: return -1;
0364:
0365: int p = firstNonWhitespaceBackward(document, pos - 1,
0366: partitioning, -1);
0367:
0368: if (p == -1)
0369: return -1;
0370:
0371: try {
0372:
0373: char ch = document.getChar(p);
0374: if (ch != '=' && ch != ']')
0375: return -1;
0376:
0377: if (p == 0)
0378: return offset;
0379:
0380: p = firstNonWhitespaceBackward(document, p - 1,
0381: partitioning, -1);
0382: if (p == -1)
0383: return -1;
0384:
0385: ch = document.getChar(p);
0386: if (Character.isJavaIdentifierPart(ch) || ch == ']'
0387: || ch == '[')
0388: return offset;
0389:
0390: } catch (BadLocationException e) {
0391: }
0392: return -1;
0393: }
0394:
0395: /**
0396: * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
0397: * <code>doc</code> involving a keyword taking a block after it. These are: <code>try</code>,
0398: * <code>do</code>, <code>synchronized</code>, <code>static</code>, <code>finally</code>, or <code>else</code>.
0399: *
0400: * @param doc the document being modified
0401: * @param line the current line under investigation
0402: * @param offset the offset of the caret position, relative to the line start.
0403: * @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
0404: */
0405: private static int computeAfterTryDoElse(IDocument doc,
0406: ITextSelection line, int offset) {
0407: // search backward while WS, find 'try', 'do', 'else' in default partition
0408: int p = offset + line.getOffset();
0409: p = firstWhitespaceToRight(doc, p);
0410: if (p == -1)
0411: return -1;
0412: p--;
0413:
0414: if (looksLike(doc, p, "try") //$NON-NLS-1$
0415: || looksLike(doc, p, "do") //$NON-NLS-1$
0416: || looksLike(doc, p, "synchronized") //$NON-NLS-1$
0417: || looksLike(doc, p, "static") //$NON-NLS-1$
0418: || looksLike(doc, p, "finally") //$NON-NLS-1$
0419: || looksLike(doc, p, "else")) //$NON-NLS-1$
0420: return p + 1 - line.getOffset();
0421:
0422: return -1;
0423: }
0424:
0425: /**
0426: * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
0427: * <code>document</code> with a expression in parenthesis that will take a block after the closing parenthesis.
0428: *
0429: * @param document the document being modified
0430: * @param line the current line under investigation
0431: * @param offset the offset of the caret position, relative to the line start.
0432: * @param partitioning the document partitioning
0433: * @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
0434: */
0435: private static int computeAfterParenthesis(IDocument document,
0436: ITextSelection line, int offset, String partitioning) {
0437: // find the opening parenthesis for every closing parenthesis on the current line after offset
0438: // return the position behind the closing parenthesis if it looks like a method declaration
0439: // or an expression for an if, while, for, catch statement
0440: int pos = offset + line.getOffset();
0441: int length = line.getOffset() + line.getLength();
0442: int scanTo = scanForward(document, pos, partitioning, length,
0443: '}');
0444: if (scanTo == -1)
0445: scanTo = length;
0446:
0447: int closingParen = findClosingParenToLeft(document, pos,
0448: partitioning) - 1;
0449:
0450: while (true) {
0451: int startScan = closingParen + 1;
0452: closingParen = scanForward(document, startScan,
0453: partitioning, scanTo, ')');
0454: if (closingParen == -1)
0455: break;
0456:
0457: int openingParen = findOpeningParenMatch(document,
0458: closingParen, partitioning);
0459:
0460: // no way an expression at the beginning of the document can mean anything
0461: if (openingParen < 1)
0462: break;
0463:
0464: // only select insert positions for parenthesis currently embracing the caret
0465: if (openingParen > pos)
0466: continue;
0467:
0468: if (looksLikeAnonymousClassDef(document, openingParen - 1,
0469: partitioning))
0470: return closingParen + 1 - line.getOffset();
0471:
0472: if (looksLikeIfWhileForCatch(document, openingParen - 1,
0473: partitioning))
0474: return closingParen + 1 - line.getOffset();
0475:
0476: if (looksLikeMethodDecl(document, openingParen - 1,
0477: partitioning))
0478: return closingParen + 1 - line.getOffset();
0479:
0480: }
0481:
0482: return -1;
0483: }
0484:
0485: /**
0486: * Finds a closing parenthesis to the left of <code>position</code> in document, where that parenthesis is only
0487: * separated by whitespace from <code>position</code>. If no such parenthesis can be found, <code>position</code> is returned.
0488: *
0489: * @param document the document being modified
0490: * @param position the first character position in <code>document</code> to be considered
0491: * @param partitioning the document partitioning
0492: * @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
0493: */
0494: private static int findClosingParenToLeft(IDocument document,
0495: int position, String partitioning) {
0496: final char CLOSING_PAREN = ')';
0497: try {
0498: if (position < 1)
0499: return position;
0500:
0501: int nonWS = firstNonWhitespaceBackward(document,
0502: position - 1, partitioning, -1);
0503: if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
0504: return nonWS;
0505: } catch (BadLocationException e1) {
0506: }
0507: return position;
0508: }
0509:
0510: /**
0511: * Finds the first whitespace character position to the right of (and including) <code>position</code>.
0512: *
0513: * @param document the document being modified
0514: * @param position the first character position in <code>document</code> to be considered
0515: * @return the position of a whitespace character greater or equal than <code>position</code> separated only by whitespace, or -1 if none found
0516: */
0517: private static int firstWhitespaceToRight(IDocument document,
0518: int position) {
0519: int length = document.getLength();
0520: Assert.isTrue(position >= 0);
0521: Assert.isTrue(position <= length);
0522:
0523: try {
0524: while (position < length) {
0525: char ch = document.getChar(position);
0526: if (Character.isWhitespace(ch))
0527: return position;
0528: position++;
0529: }
0530: return position;
0531: } catch (BadLocationException e) {
0532: }
0533: return -1;
0534: }
0535:
0536: /**
0537: * Finds the highest position in <code>document</code> such that the position is <= <code>position</code>
0538: * and > <code>bound</code> and <code>Character.isWhitespace(document.getChar(pos))</code> evaluates to <code>false</code>
0539: * and the position is in the default partition.
0540: *
0541: * @param document the document being modified
0542: * @param position the first character position in <code>document</code> to be considered
0543: * @param partitioning the document partitioning
0544: * @param bound the first position in <code>document</code> to not consider any more, with <code>bound</code> < <code>position</code>
0545: * @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
0546: */
0547: private static int firstNonWhitespaceBackward(IDocument document,
0548: int position, String partitioning, int bound) {
0549: Assert.isTrue(position < document.getLength());
0550: Assert.isTrue(bound >= -1);
0551:
0552: try {
0553: while (position > bound) {
0554: char ch = document.getChar(position);
0555: if (!Character.isWhitespace(ch)
0556: && isDefaultPartition(document, position,
0557: partitioning))
0558: return position;
0559: position--;
0560: }
0561: } catch (BadLocationException e) {
0562: }
0563: return -1;
0564: }
0565:
0566: /**
0567: * Finds the smallest position in <code>document</code> such that the position is >= <code>position</code>
0568: * and < <code>bound</code> and <code>Character.isWhitespace(document.getChar(pos))</code> evaluates to <code>false</code>
0569: * and the position is in the default partition.
0570: *
0571: * @param document the document being modified
0572: * @param position the first character position in <code>document</code> to be considered
0573: * @param partitioning the document partitioning
0574: * @param bound the first position in <code>document</code> to not consider any more, with <code>bound</code> > <code>position</code>
0575: * @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
0576: */
0577: private static int firstNonWhitespaceForward(IDocument document,
0578: int position, String partitioning, int bound) {
0579: Assert.isTrue(position >= 0);
0580: Assert.isTrue(bound <= document.getLength());
0581:
0582: try {
0583: while (position < bound) {
0584: char ch = document.getChar(position);
0585: if (!Character.isWhitespace(ch)
0586: && isDefaultPartition(document, position,
0587: partitioning))
0588: return position;
0589: position++;
0590: }
0591: } catch (BadLocationException e) {
0592: }
0593: return -1;
0594: }
0595:
0596: /**
0597: * Finds the highest position in <code>document</code> such that the position is <= <code>position</code>
0598: * and > <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code> for at least one
0599: * ch in <code>chars</code> and the position is in the default partition.
0600: *
0601: * @param document the document being modified
0602: * @param position the first character position in <code>document</code> to be considered
0603: * @param partitioning the document partitioning
0604: * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
0605: * @param chars an array of <code>char</code> to search for
0606: * @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
0607: */
0608: private static int scanBackward(IDocument document, int position,
0609: String partitioning, int bound, char[] chars) {
0610: Assert.isTrue(bound >= -1);
0611: Assert.isTrue(position < document.getLength());
0612:
0613: Arrays.sort(chars);
0614:
0615: try {
0616: while (position > bound) {
0617:
0618: if (Arrays.binarySearch(chars, document
0619: .getChar(position)) >= 0
0620: && isDefaultPartition(document, position,
0621: partitioning))
0622: return position;
0623:
0624: position--;
0625: }
0626: } catch (BadLocationException e) {
0627: }
0628: return -1;
0629: }
0630:
0631: // /**
0632: // * Finds the highest position in <code>document</code> such that the position is <= <code>position</code>
0633: // * and > <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code>
0634: // * and the position is in the default partition.
0635: // *
0636: // * @param document the document being modified
0637: // * @param position the first character position in <code>document</code> to be considered
0638: // * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
0639: // * @param chars an array of <code>char</code> to search for
0640: // * @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
0641: // */
0642: // private static int scanBackward(IDocument document, int position, int bound, char ch) {
0643: // return scanBackward(document, position, bound, new char[] {ch});
0644: // }
0645: //
0646: /**
0647: * Finds the lowest position in <code>document</code> such that the position is >= <code>position</code>
0648: * and < <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code> for at least one
0649: * ch in <code>chars</code> and the position is in the default partition.
0650: *
0651: * @param document the document being modified
0652: * @param position the first character position in <code>document</code> to be considered
0653: * @param partitioning the document partitioning
0654: * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
0655: * @param chars an array of <code>char</code> to search for
0656: * @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
0657: */
0658: private static int scanForward(IDocument document, int position,
0659: String partitioning, int bound, char[] chars) {
0660: Assert.isTrue(position >= 0);
0661: Assert.isTrue(bound <= document.getLength());
0662:
0663: Arrays.sort(chars);
0664:
0665: try {
0666: while (position < bound) {
0667:
0668: if (Arrays.binarySearch(chars, document
0669: .getChar(position)) >= 0
0670: && isDefaultPartition(document, position,
0671: partitioning))
0672: return position;
0673:
0674: position++;
0675: }
0676: } catch (BadLocationException e) {
0677: }
0678: return -1;
0679: }
0680:
0681: /**
0682: * Finds the lowest position in <code>document</code> such that the position is >= <code>position</code>
0683: * and < <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code>
0684: * and the position is in the default partition.
0685: *
0686: * @param document the document being modified
0687: * @param position the first character position in <code>document</code> to be considered
0688: * @param partitioning the document partitioning
0689: * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
0690: * @param ch a <code>char</code> to search for
0691: * @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
0692: */
0693: private static int scanForward(IDocument document, int position,
0694: String partitioning, int bound, char ch) {
0695: return scanForward(document, position, partitioning, bound,
0696: new char[] { ch });
0697: }
0698:
0699: /**
0700: * Checks whether the content of <code>document</code> in the range (<code>offset</code>, <code>length</code>)
0701: * contains the <code>new</code> keyword.
0702: *
0703: * @param document the document being modified
0704: * @param offset the first character position in <code>document</code> to be considered
0705: * @param length the length of the character range to be considered
0706: * @param partitioning the document partitioning
0707: * @return <code>true</code> if the specified character range contains a <code>new</code> keyword, <code>false</code> otherwise.
0708: */
0709: private static boolean isNewMatch(IDocument document, int offset,
0710: int length, String partitioning) {
0711: Assert.isTrue(length >= 0);
0712: Assert.isTrue(offset >= 0);
0713: Assert.isTrue(offset + length < document.getLength() + 1);
0714:
0715: try {
0716: String text = document.get(offset, length);
0717: int pos = text.indexOf("new"); //$NON-NLS-1$
0718:
0719: while (pos != -1
0720: && !isDefaultPartition(document, pos + offset,
0721: partitioning))
0722: pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
0723:
0724: if (pos < 0)
0725: return false;
0726:
0727: if (pos != 0
0728: && Character.isJavaIdentifierPart(text
0729: .charAt(pos - 1)))
0730: return false;
0731:
0732: if (pos + 3 < length
0733: && Character.isJavaIdentifierPart(text
0734: .charAt(pos + 3)))
0735: return false;
0736:
0737: return true;
0738:
0739: } catch (BadLocationException e) {
0740: }
0741: return false;
0742: }
0743:
0744: /**
0745: * Checks whether the content of <code>document</code> at <code>position</code> looks like an
0746: * anonymous class definition. <code>position</code> must be to the left of the opening
0747: * parenthesis of the definition's parameter list.
0748: *
0749: * @param document the document being modified
0750: * @param position the first character position in <code>document</code> to be considered
0751: * @param partitioning the document partitioning
0752: * @return <code>true</code> if the content of <code>document</code> looks like an anonymous class definition, <code>false</code> otherwise
0753: */
0754: private static boolean looksLikeAnonymousClassDef(
0755: IDocument document, int position, String partitioning) {
0756: int previousCommaParenEqual = scanBackward(document,
0757: position - 1, partitioning, -1, new char[] { ',', '(',
0758: '=' });
0759: if (previousCommaParenEqual == -1
0760: || position < previousCommaParenEqual + 5) // 2 for borders, 3 for "new"
0761: return false;
0762:
0763: if (isNewMatch(document, previousCommaParenEqual + 1, position
0764: - previousCommaParenEqual - 2, partitioning))
0765: return true;
0766:
0767: return false;
0768: }
0769:
0770: /**
0771: * Checks whether <code>position</code> resides in a default (Java) partition of <code>document</code>.
0772: *
0773: * @param document the document being modified
0774: * @param position the position to be checked
0775: * @param partitioning the document partitioning
0776: * @return <code>true</code> if <code>position</code> is in the default partition of <code>document</code>, <code>false</code> otherwise
0777: */
0778: private static boolean isDefaultPartition(IDocument document,
0779: int position, String partitioning) {
0780: Assert.isTrue(position >= 0);
0781: Assert.isTrue(position <= document.getLength());
0782:
0783: try {
0784: // don't use getPartition2 since we're interested in the scanned character's partition
0785: ITypedRegion region = TextUtilities.getPartition(document,
0786: partitioning, position, false);
0787: return region.getType().equals(
0788: IDocument.DEFAULT_CONTENT_TYPE);
0789:
0790: } catch (BadLocationException e) {
0791: }
0792:
0793: return false;
0794: }
0795:
0796: /**
0797: * Finds the position of the parenthesis matching the closing parenthesis at <code>position</code>.
0798: *
0799: * @param document the document being modified
0800: * @param position the position in <code>document</code> of a closing parenthesis
0801: * @param partitioning the document partitioning
0802: * @return the position in <code>document</code> of the matching parenthesis, or -1 if none can be found
0803: */
0804: private static int findOpeningParenMatch(IDocument document,
0805: int position, String partitioning) {
0806: final char CLOSING_PAREN = ')';
0807: final char OPENING_PAREN = '(';
0808:
0809: Assert.isTrue(position < document.getLength());
0810: Assert.isTrue(position >= 0);
0811: Assert.isTrue(isDefaultPartition(document, position,
0812: partitioning));
0813:
0814: try {
0815:
0816: Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
0817:
0818: int depth = 1;
0819: while (true) {
0820: position = scanBackward(document, position - 1,
0821: partitioning, -1, new char[] { CLOSING_PAREN,
0822: OPENING_PAREN });
0823: if (position == -1)
0824: return -1;
0825:
0826: if (document.getChar(position) == CLOSING_PAREN)
0827: depth++;
0828: else
0829: depth--;
0830:
0831: if (depth == 0)
0832: return position;
0833: }
0834:
0835: } catch (BadLocationException e) {
0836: return -1;
0837: }
0838: }
0839:
0840: /**
0841: * Checks whether, to the left of <code>position</code> and separated only by whitespace,
0842: * <code>document</code> contains a keyword taking a parameter list and a block after it.
0843: * These are: <code>if</code>, <code>while</code>, <code>catch</code>, <code>for</code>, <code>synchronized</code>, <code>switch</code>.
0844: *
0845: * @param document the document being modified
0846: * @param position the first character position in <code>document</code> to be considered
0847: * @param partitioning the document partitioning
0848: * @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
0849: */
0850: private static boolean looksLikeIfWhileForCatch(IDocument document,
0851: int position, String partitioning) {
0852: position = firstNonWhitespaceBackward(document, position,
0853: partitioning, -1);
0854: if (position == -1)
0855: return false;
0856:
0857: return looksLike(document, position, "if") //$NON-NLS-1$
0858: || looksLike(document, position, "while") //$NON-NLS-1$
0859: || looksLike(document, position, "catch") //$NON-NLS-1$
0860: || looksLike(document, position, "synchronized") //$NON-NLS-1$
0861: || looksLike(document, position, "switch") //$NON-NLS-1$
0862: || looksLike(document, position, "for"); //$NON-NLS-1$
0863: }
0864:
0865: /**
0866: * Checks whether code>document</code> contains the <code>String</code> <code>like</code> such
0867: * that its last character is at <code>position</code>. If <code>like</code> starts with a
0868: * identifier part (as determined by {@link Character#isJavaIdentifierPart(char)}), it is also made
0869: * sure that <code>like</code> is preceded by some non-identifier character or stands at the
0870: * document start.
0871: *
0872: * @param document the document being modified
0873: * @param position the first character position in <code>document</code> to be considered
0874: * @param like the <code>String</code> to look for.
0875: * @return <code>true</code> if <code>document</code> contains <code>like</code> such that it ends at <code>position</code>, <code>false</code> otherwise
0876: */
0877: private static boolean looksLike(IDocument document, int position,
0878: String like) {
0879: int length = like.length();
0880: if (position < length - 1)
0881: return false;
0882:
0883: try {
0884: if (!like.equals(document
0885: .get(position - length + 1, length)))
0886: return false;
0887:
0888: if (position >= length
0889: && Character.isJavaIdentifierPart(like.charAt(0))
0890: && Character.isJavaIdentifierPart(document
0891: .getChar(position - length)))
0892: return false;
0893:
0894: } catch (BadLocationException e) {
0895: return false;
0896: }
0897:
0898: return true;
0899: }
0900:
0901: /**
0902: * Checks whether the content of <code>document</code> at <code>position</code> looks like a
0903: * method declaration header (i.e. only the return type and method name). <code>position</code>
0904: * must be just left of the opening parenthesis of the parameter list.
0905: *
0906: * @param document the document being modified
0907: * @param position the first character position in <code>document</code> to be considered
0908: * @param partitioning the document partitioning
0909: * @return <code>true</code> if the content of <code>document</code> looks like a method definition, <code>false</code> otherwise
0910: */
0911: private static boolean looksLikeMethodDecl(IDocument document,
0912: int position, String partitioning) {
0913:
0914: // method name
0915: position = eatIdentToLeft(document, position, partitioning);
0916: if (position < 1)
0917: return false;
0918:
0919: position = eatBrackets(document, position - 1, partitioning);
0920: if (position < 1)
0921: return false;
0922:
0923: position = eatIdentToLeft(document, position - 1, partitioning);
0924:
0925: return position != -1;
0926: }
0927:
0928: /**
0929: * From <code>position</code> to the left, eats any whitespace and then a pair of brackets
0930: * as used to declare an array return type like <pre>String [ ]</pre>.
0931: * The return value is either the position of the opening bracket or <code>position</code> if no
0932: * pair of brackets can be parsed.
0933: *
0934: * @param document the document being modified
0935: * @param position the first character position in <code>document</code> to be considered
0936: * @param partitioning the document partitioning
0937: * @return the smallest character position of bracket pair or <code>position</code>
0938: */
0939: private static int eatBrackets(IDocument document, int position,
0940: String partitioning) {
0941: // accept array return type
0942: int pos = firstNonWhitespaceBackward(document, position,
0943: partitioning, -1);
0944: try {
0945: if (pos > 1 && document.getChar(pos) == ']') {
0946: pos = firstNonWhitespaceBackward(document, pos - 1,
0947: partitioning, -1);
0948: if (pos > 0 && document.getChar(pos) == '[')
0949: return pos;
0950: }
0951: } catch (BadLocationException e) {
0952: // won't happen
0953: }
0954: return position;
0955: }
0956:
0957: /**
0958: * From <code>position</code> to the left, eats any whitespace and the first identifier, returning
0959: * the position of the first identifier character (in normal read order).
0960: * <p>When called on a document with content <code>" some string "</code> and positionition 13, the
0961: * return value will be 6 (the first letter in <code>string</code>).
0962: * </p>
0963: *
0964: * @param document the document being modified
0965: * @param position the first character position in <code>document</code> to be considered
0966: * @param partitioning the document partitioning
0967: * @return the smallest character position of an identifier or -1 if none can be found; always <= <code>position</code>
0968: */
0969: private static int eatIdentToLeft(IDocument document, int position,
0970: String partitioning) {
0971: if (position < 0)
0972: return -1;
0973: Assert.isTrue(position < document.getLength());
0974:
0975: int p = firstNonWhitespaceBackward(document, position,
0976: partitioning, -1);
0977: if (p == -1)
0978: return -1;
0979:
0980: try {
0981: while (p >= 0) {
0982:
0983: char ch = document.getChar(p);
0984: if (Character.isJavaIdentifierPart(ch)) {
0985: p--;
0986: continue;
0987: }
0988:
0989: // length must be > 0
0990: if (Character.isWhitespace(ch) && p != position)
0991: return p + 1;
0992: else
0993: return -1;
0994:
0995: }
0996:
0997: // start of document reached
0998: return 0;
0999:
1000: } catch (BadLocationException e) {
1001: }
1002: return -1;
1003: }
1004:
1005: /**
1006: * Returns a position in the first java partition after the last non-empty and non-comment partition.
1007: * There is no non-whitespace from the returned position to the end of the partition it is contained in.
1008: *
1009: * @param document the document being modified
1010: * @param line the line under investigation
1011: * @param offset the caret offset into <code>line</code>
1012: * @param partitioning the document partitioning
1013: * @return the position of the next Java partition, or the end of <code>line</code>
1014: */
1015: private static int nextPartitionOrLineEnd(IDocument document,
1016: ITextSelection line, int offset, String partitioning) {
1017: // run relative to document
1018: final int docOffset = offset + line.getOffset();
1019: final int eol = line.getOffset() + line.getLength();
1020: int nextPartitionPos = eol; // init with line end
1021: int validPosition = docOffset;
1022:
1023: try {
1024: ITypedRegion partition = TextUtilities.getPartition(
1025: document, partitioning, nextPartitionPos, true);
1026: validPosition = getValidPositionForPartition(document,
1027: partition, eol);
1028: while (validPosition == -1) {
1029: nextPartitionPos = partition.getOffset() - 1;
1030: if (nextPartitionPos < docOffset) {
1031: validPosition = docOffset;
1032: break;
1033: }
1034: partition = TextUtilities.getPartition(document,
1035: partitioning, nextPartitionPos, false);
1036: validPosition = getValidPositionForPartition(document,
1037: partition, eol);
1038: }
1039: } catch (BadLocationException e) {
1040: }
1041:
1042: validPosition = Math.max(validPosition, docOffset);
1043: // make relative to line
1044: validPosition -= line.getOffset();
1045: return validPosition;
1046: }
1047:
1048: /**
1049: * Returns a valid insert location (except for whitespace) in <code>partition</code> or -1 if
1050: * there is no valid insert location.
1051: * An valid insert location is right after any java string or character partition, or at the end
1052: * of a java default partition, but never behind <code>maxOffset</code>. Comment partitions or
1053: * empty java partitions do never yield valid insert positions.
1054: *
1055: * @param doc the document being modified
1056: * @param partition the current partition
1057: * @param maxOffset the maximum offset to consider
1058: * @return a valid insert location in <code>partition</code>, or -1 if there is no valid insert location
1059: */
1060: private static int getValidPositionForPartition(IDocument doc,
1061: ITypedRegion partition, int maxOffset) {
1062: final int INVALID = -1;
1063:
1064: if (IJavaPartitions.JAVA_DOC.equals(partition.getType()))
1065: return INVALID;
1066: if (IJavaPartitions.JAVA_MULTI_LINE_COMMENT.equals(partition
1067: .getType()))
1068: return INVALID;
1069: if (IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(partition
1070: .getType()))
1071: return INVALID;
1072:
1073: int endOffset = Math.min(maxOffset, partition.getOffset()
1074: + partition.getLength());
1075:
1076: if (IJavaPartitions.JAVA_CHARACTER.equals(partition.getType()))
1077: return endOffset;
1078: if (IJavaPartitions.JAVA_STRING.equals(partition.getType()))
1079: return endOffset;
1080: if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1081: try {
1082: if (doc.get(partition.getOffset(),
1083: endOffset - partition.getOffset()).trim()
1084: .length() == 0)
1085: return INVALID;
1086: else
1087: return endOffset;
1088: } catch (BadLocationException e) {
1089: return INVALID;
1090: }
1091: }
1092: // default: we don't know anything about the partition - assume valid
1093: return endOffset;
1094: }
1095:
1096: /**
1097: * Determines whether the current line contains a for statement.
1098: * Algorithm: any "for" word in the line is a positive, "for" contained in a string literal will
1099: * produce a false positive.
1100: *
1101: * @param line the line where the change is being made
1102: * @param offset the position of the caret
1103: * @return <code>true</code> if <code>line</code> contains <code>for</code>, <code>false</code> otherwise
1104: */
1105: private static boolean isForStatement(String line, int offset) {
1106: /* searching for (^|\s)for(\s|$) */
1107: int forPos = line.indexOf("for"); //$NON-NLS-1$
1108: if (forPos != -1) {
1109: if ((forPos == 0 || !Character.isJavaIdentifierPart(line
1110: .charAt(forPos - 1)))
1111: && (line.length() == forPos + 3 || !Character
1112: .isJavaIdentifierPart(line
1113: .charAt(forPos + 3))))
1114: return true;
1115: }
1116: return false;
1117: }
1118:
1119: /**
1120: * Returns the position in <code>text</code> after which there comes only whitespace, up to
1121: * <code>offset</code>.
1122: *
1123: * @param text the text being searched
1124: * @param offset the maximum offset to search for
1125: * @return the smallest value <code>v</code> such that <code>text.substring(v, offset).trim() == 0</code>
1126: */
1127: private static int startOfWhitespaceBeforeOffset(String text,
1128: int offset) {
1129: int i = Math.min(offset, text.length());
1130: for (; i >= 1; i--) {
1131: if (!Character.isWhitespace(text.charAt(i - 1)))
1132: break;
1133: }
1134: return i;
1135: }
1136: }
|