1 /*******************************************************************************
2 * Copyright (c) 2000, 2008 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package org.eclipse.jdt.internal.ui.text.java;
13 import java.util.Arrays;
15 import org.eclipse.core.runtime.Assert;
17 import org.eclipse.text.edits.DeleteEdit;
18 import org.eclipse.text.edits.MalformedTreeException;
19 import org.eclipse.text.edits.ReplaceEdit;
20 import org.eclipse.text.edits.TextEdit;
22 import org.eclipse.jface.preference.IPreferenceStore;
24 import org.eclipse.jface.text.BadLocationException;
25 import org.eclipse.jface.text.DocumentCommand;
26 import org.eclipse.jface.text.IAutoEditStrategy;
27 import org.eclipse.jface.text.IDocument;
28 import org.eclipse.jface.text.IRegion;
29 import org.eclipse.jface.text.ITextSelection;
30 import org.eclipse.jface.text.ITypedRegion;
31 import org.eclipse.jface.text.Region;
32 import org.eclipse.jface.text.TextSelection;
33 import org.eclipse.jface.text.TextUtilities;
35 import org.eclipse.ui.IEditorPart;
36 import org.eclipse.ui.IWorkbenchPage;
38 import org.eclipse.ui.texteditor.ITextEditorExtension2;
39 import org.eclipse.ui.texteditor.ITextEditorExtension3;
41 import org.eclipse.jdt.ui.PreferenceConstants;
42 import org.eclipse.jdt.ui.text.IJavaPartitions;
44 import org.eclipse.jdt.internal.ui.JavaPlugin;
45 import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
46 import org.eclipse.jdt.internal.ui.text.SmartBackspaceManager;
47 import org.eclipse.jdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
50 * Modifies <code>DocumentCommand</code>s inserting semicolons and opening braces to place them
51 * smartly, i.e. moving them to the end of a line if that is what the user expects.
53 * <p>In practice, semicolons and braces (and the caret) are moved to the end of the line if they are typed
54 * anywhere except for semicolons in a <code>for</code> statements definition. If the line contains a semicolon
55 * or brace after the current caret position, the cursor is moved after it.</p>
57 * @see org.eclipse.jface.text.DocumentCommand
60 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
62 /** String representation of a semicolon. */
63 private static final String SEMICOLON= ";"; //$NON-NLS-1$
64 /** Char representation of a semicolon. */
65 private static final char SEMICHAR= ';';
66 /** String represenattion of a opening brace. */
67 private static final String BRACE= "{"; //$NON-NLS-1$
68 /** Char representation of a opening brace */
69 private static final char BRACECHAR= '{';
71 private char fCharacter;
72 private String fPartitioning;
75 * Creates a new SmartSemicolonAutoEditStrategy.
77 * @param partitioning the document partitioning
79 public SmartSemicolonAutoEditStrategy(String partitioning) {
80 fPartitioning= partitioning;
84 * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
86 public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
88 // also customize if <code>doit</code> is false (so it works in code completion situations)
92 if (command.text == null)
95 if (command.text.equals(SEMICOLON))
97 else if (command.text.equals(BRACE))
98 fCharacter= BRACECHAR;
102 IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore();
103 if (fCharacter == SEMICHAR && !store.getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
105 if (fCharacter == BRACECHAR && !store.getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
108 IWorkbenchPage page= JavaPlugin.getActivePage();
111 IEditorPart part= page.getActiveEditor();
112 if (!(part instanceof CompilationUnitEditor))
114 CompilationUnitEditor editor= (CompilationUnitEditor)part;
115 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT || !editor.isEditable())
117 ITextEditorExtension2 extension= (ITextEditorExtension2)editor.getAdapter(ITextEditorExtension2.class);
118 if (extension != null && !extension.validateEditorInputState())
120 if (isMultilineSelection(document, command))
123 // 1: find concerned line / position in java code, location in statement
124 int pos= command.offset;
127 IRegion l= document.getLineInformationOfOffset(pos);
128 line= new TextSelection(document, l.getOffset(), l.getLength());
129 } catch (BadLocationException e) {
133 // 2: choose action based on findings (is for-Statement?)
134 // for now: compute the best position to insert the new character
135 int positionInLine= computeCharacterPosition(document, line, pos - line.getOffset(), fCharacter, fPartitioning);
136 int position= positionInLine + line.getOffset();
138 // never position before the current position!
142 // never double already existing content
143 if (alreadyPresent(document, fCharacter, position))
146 // don't do special processing if what we do is actually the normal behaviour
147 String insertion= adjustSpacing(document, position, fCharacter);
148 if (command.offset == position && insertion.equals(command.text))
153 final SmartBackspaceManager manager= (SmartBackspaceManager) editor.getAdapter(SmartBackspaceManager.class);
154 if (manager != null && JavaPlugin.getDefault().getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
155 TextEdit e1= new ReplaceEdit(command.offset, command.text.length(), document.get(command.offset, command.length));
156 UndoSpec s1= new UndoSpec(command.offset + command.text.length(),
157 new Region(command.offset, 0),
162 DeleteEdit smart= new DeleteEdit(position, insertion.length());
163 ReplaceEdit raw= new ReplaceEdit(command.offset, command.length, command.text);
164 UndoSpec s2= new UndoSpec(position + insertion.length(),
165 new Region(command.offset + command.text.length(), 0),
166 new TextEdit[] {smart, raw},
169 manager.register(s2);
173 command.offset= position;
175 command.caretOffset= position;
176 command.text= insertion;
179 } catch (MalformedTreeException e) {
181 } catch (BadLocationException e) {
189 * Returns <code>true</code> if the document command is applied on a multi
190 * line selection, <code>false</code> otherwise.
192 * @param document the document
193 * @param command the command
194 * @return <code>true</code> if <code>command</code> is a multiline command
196 private boolean isMultilineSelection(IDocument document, DocumentCommand command) {
198 return document.getNumberOfLines(command.offset, command.length) > 1;
199 } catch (BadLocationException e) {
206 * Adds a space before a brace if it is inserted after a parenthesis, equal sign, or one
207 * of the keywords <code>try, else, do</code>.
209 * @param doc the document we are working on
210 * @param position the insert position of <code>character</code>
211 * @param character the character to be inserted
212 * @return a <code>String</code> consisting of <code>character</code> plus any additional spacing
214 private String adjustSpacing(IDocument doc, int position, char character) {
215 if (character == BRACECHAR) {
216 if (position > 0 && position <= doc.getLength()) {
217 int pos= position - 1;
218 if (looksLike(doc, pos, ")") //$NON-NLS-1$
219 || looksLike(doc, pos, "=") //$NON-NLS-1$
220 || looksLike(doc, pos, "]") //$NON-NLS-1$
221 || looksLike(doc, pos, "try") //$NON-NLS-1$
222 || looksLike(doc, pos, "else") //$NON-NLS-1$
223 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
224 || looksLike(doc, pos, "static") //$NON-NLS-1$
225 || looksLike(doc, pos, "finally") //$NON-NLS-1$
226 || looksLike(doc, pos, "do")) //$NON-NLS-1$
227 return new String(new char[] { ' ', character });
231 return new String(new char[] { character });
235 * Checks whether a character to be inserted is already present at the insert location (perhaps
236 * separated by some whitespace from <code>position</code>.
238 * @param document the document we are working on
239 * @param position the insert position of <code>ch</code>
240 * @param ch the character to be inserted
241 * @return <code>true</code> if <code>ch</code> is already present at <code>location</code>, <code>false</code> otherwise
243 private boolean alreadyPresent(IDocument document, char ch, int position) {
244 int pos= firstNonWhitespaceForward(document, position, fPartitioning, document.getLength());
246 if (pos != -1 && document.getChar(pos) == ch)
248 } catch (BadLocationException e) {
255 * Computes the next insert position of the given character in the current line.
257 * @param document the document we are working on
258 * @param line the line where the change is being made
259 * @param offset the position of the caret in the line when <code>character</code> was typed
260 * @param character the character to look for
261 * @param partitioning the document partitioning
262 * @return the position where <code>character</code> should be inserted / replaced
264 protected static int computeCharacterPosition(IDocument document, ITextSelection line, int offset, char character, String partitioning) {
265 String text= line.getText();
270 if (character == BRACECHAR) {
272 insertPos= computeArrayInitializationPos(document, line, offset, partitioning);
274 if (insertPos == -1) {
275 insertPos= computeAfterTryDoElse(document, line, offset);
278 if (insertPos == -1) {
279 insertPos= computeAfterParenthesis(document, line, offset, partitioning);
282 } else if (character == SEMICHAR) {
284 if (isForStatement(text, offset)) {
285 insertPos= -1; // don't do anything in for statements, as semis are vital part of these
287 int nextPartitionPos= nextPartitionOrLineEnd(document, line, offset, partitioning);
288 insertPos= startOfWhitespaceBeforeOffset(text, nextPartitionPos);
289 // if there is a semi present, return its location as alreadyPresent() will take it out this way.
290 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
291 insertPos= insertPos - 1;
292 else if (insertPos > 0 && text.charAt(insertPos - 1) == '}') {
293 int opening= scanBackward(document, insertPos - 1 + line.getOffset(), partitioning, -1, new char[] { '{' });
294 if (opening > -1 && opening < offset + line.getOffset()) {
295 if (computeArrayInitializationPos(document, line, opening - line.getOffset(), partitioning) == -1) {
303 Assert.isTrue(false);
311 * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
312 * <code>document</code> that looks like being the RHS of an assignment or like an array definition.
314 * @param document the document being modified
315 * @param line the current line under investigation
316 * @param offset the offset of the caret position, relative to the line start.
317 * @param partitioning the document partitioning
318 * @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
320 private static int computeArrayInitializationPos(IDocument document, ITextSelection line, int offset, String partitioning) {
321 // search backward while WS, find = (not != <= >= ==) in default partition
322 int pos= offset + line.getOffset();
327 int p= firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
334 char ch= document.getChar(p);
335 if (ch != '=' && ch != ']')
341 p= firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
345 ch= document.getChar(p);
346 if (Character.isJavaIdentifierPart(ch) || ch == ']' || ch == '[')
349 } catch (BadLocationException e) {
355 * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
356 * <code>doc</code> involving a keyword taking a block after it. These are: <code>try</code>,
357 * <code>do</code>, <code>synchronized</code>, <code>static</code>, <code>finally</code>, or <code>else</code>.
359 * @param doc the document being modified
360 * @param line the current line under investigation
361 * @param offset the offset of the caret position, relative to the line start.
362 * @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
364 private static int computeAfterTryDoElse(IDocument doc, ITextSelection line, int offset) {
365 // search backward while WS, find 'try', 'do', 'else' in default partition
366 int p= offset + line.getOffset();
367 p= firstWhitespaceToRight(doc, p);
372 if (looksLike(doc, p, "try") //$NON-NLS-1$
373 || looksLike(doc, p, "do") //$NON-NLS-1$
374 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
375 || looksLike(doc, p, "static") //$NON-NLS-1$
376 || looksLike(doc, p, "finally") //$NON-NLS-1$
377 || looksLike(doc, p, "else")) //$NON-NLS-1$
378 return p + 1 - line.getOffset();
384 * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
385 * <code>document</code> with a expression in parenthesis that will take a block after the closing parenthesis.
387 * @param document the document being modified
388 * @param line the current line under investigation
389 * @param offset the offset of the caret position, relative to the line start.
390 * @param partitioning the document partitioning
391 * @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
393 private static int computeAfterParenthesis(IDocument document, ITextSelection line, int offset, String partitioning) {
394 // find the opening parenthesis for every closing parenthesis on the current line after offset
395 // return the position behind the closing parenthesis if it looks like a method declaration
396 // or an expression for an if, while, for, catch statement
397 int pos= offset + line.getOffset();
398 int length= line.getOffset() + line.getLength();
399 int scanTo= scanForward(document, pos, partitioning, length, '}');
403 int closingParen= findClosingParenToLeft(document, pos, partitioning) - 1;
406 int startScan= closingParen + 1;
407 closingParen= scanForward(document, startScan, partitioning, scanTo, ')');
408 if (closingParen == -1)
411 int openingParen= findOpeningParenMatch(document, closingParen, partitioning);
413 // no way an expression at the beginning of the document can mean anything
414 if (openingParen < 1)
417 // only select insert positions for parenthesis currently embracing the caret
418 if (openingParen > pos)
421 if (looksLikeAnonymousClassDef(document, openingParen - 1, partitioning))
422 return closingParen + 1 - line.getOffset();
424 if (looksLikeIfWhileForCatch(document, openingParen - 1, partitioning))
425 return closingParen + 1 - line.getOffset();
427 if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
428 return closingParen + 1 - line.getOffset();
436 * Finds a closing parenthesis to the left of <code>position</code> in document, where that parenthesis is only
437 * separated by whitespace from <code>position</code>. If no such parenthesis can be found, <code>position</code> is returned.
439 * @param document the document being modified
440 * @param position the first character position in <code>document</code> to be considered
441 * @param partitioning the document partitioning
442 * @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
444 private static int findClosingParenToLeft(IDocument document, int position, String partitioning) {
445 final char CLOSING_PAREN= ')';
450 int nonWS= firstNonWhitespaceBackward(document, position - 1, partitioning, -1);
451 if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
453 } catch (BadLocationException e1) {
459 * Finds the first whitespace character position to the right of (and including) <code>position</code>.
461 * @param document the document being modified
462 * @param position the first character position in <code>document</code> to be considered
463 * @return the position of a whitespace character greater or equal than <code>position</code> separated only by whitespace, or -1 if none found
465 private static int firstWhitespaceToRight(IDocument document, int position) {
466 int length= document.getLength();
467 Assert.isTrue(position >= 0);
468 Assert.isTrue(position <= length);
471 while (position < length) {
472 char ch= document.getChar(position);
473 if (Character.isWhitespace(ch))
478 } catch (BadLocationException e) {
484 * Finds the highest position in <code>document</code> such that the position is <= <code>position</code>
485 * and > <code>bound</code> and <code>Character.isWhitespace(document.getChar(pos))</code> evaluates to <code>false</code>
486 * and the position is in the default partition.
488 * @param document the document being modified
489 * @param position the first character position in <code>document</code> to be considered
490 * @param partitioning the document partitioning
491 * @param bound the first position in <code>document</code> to not consider any more, with <code>bound</code> < <code>position</code>
492 * @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
494 private static int firstNonWhitespaceBackward(IDocument document, int position, String partitioning, int bound) {
495 Assert.isTrue(position < document.getLength());
496 Assert.isTrue(bound >= -1);
499 while (position > bound) {
500 char ch= document.getChar(position);
501 if (!Character.isWhitespace(ch) && isDefaultPartition(document, position, partitioning))
505 } catch (BadLocationException e) {
511 * Finds the smallest position in <code>document</code> such that the position is >= <code>position</code>
512 * and < <code>bound</code> and <code>Character.isWhitespace(document.getChar(pos))</code> evaluates to <code>false</code>
513 * and the position is in the default partition.
515 * @param document the document being modified
516 * @param position the first character position in <code>document</code> to be considered
517 * @param partitioning the document partitioning
518 * @param bound the first position in <code>document</code> to not consider any more, with <code>bound</code> > <code>position</code>
519 * @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
521 private static int firstNonWhitespaceForward(IDocument document, int position, String partitioning, int bound) {
522 Assert.isTrue(position >= 0);
523 Assert.isTrue(bound <= document.getLength());
526 while (position < bound) {
527 char ch= document.getChar(position);
528 if (!Character.isWhitespace(ch) && isDefaultPartition(document, position, partitioning))
532 } catch (BadLocationException e) {
538 * Finds the highest position in <code>document</code> such that the position is <= <code>position</code>
539 * and > <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code> for at least one
540 * ch in <code>chars</code> and the position is in the default partition.
542 * @param document the document being modified
543 * @param position the first character position in <code>document</code> to be considered
544 * @param partitioning the document partitioning
545 * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
546 * @param chars an array of <code>char</code> to search for
547 * @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
549 private static int scanBackward(IDocument document, int position, String partitioning, int bound, char[] chars) {
550 Assert.isTrue(bound >= -1);
551 Assert.isTrue(position < document.getLength() );
556 while (position > bound) {
558 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0 && isDefaultPartition(document, position, partitioning))
563 } catch (BadLocationException e) {
569 // * Finds the highest position in <code>document</code> such that the position is <= <code>position</code>
570 // * and > <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code>
571 // * and the position is in the default partition.
573 // * @param document the document being modified
574 // * @param position the first character position in <code>document</code> to be considered
575 // * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
576 // * @param chars an array of <code>char</code> to search for
577 // * @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
579 // private static int scanBackward(IDocument document, int position, int bound, char ch) {
580 // return scanBackward(document, position, bound, new char[] {ch});
584 * Finds the lowest position in <code>document</code> such that the position is >= <code>position</code>
585 * and < <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code> for at least one
586 * ch in <code>chars</code> and the position is in the default partition.
588 * @param document the document being modified
589 * @param position the first character position in <code>document</code> to be considered
590 * @param partitioning the document partitioning
591 * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
592 * @param chars an array of <code>char</code> to search for
593 * @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
595 private static int scanForward(IDocument document, int position, String partitioning, int bound, char[] chars) {
596 Assert.isTrue(position >= 0);
597 Assert.isTrue(bound <= document.getLength());
602 while (position < bound) {
604 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0 && isDefaultPartition(document, position, partitioning))
609 } catch (BadLocationException e) {
615 * Finds the lowest position in <code>document</code> such that the position is >= <code>position</code>
616 * and < <code>bound</code> and <code>document.getChar(position) == ch</code> evaluates to <code>true</code>
617 * and the position is in the default partition.
619 * @param document the document being modified
620 * @param position the first character position in <code>document</code> to be considered
621 * @param partitioning the document partitioning
622 * @param bound the first position in <code>document</code> to not consider any more, with <code>scanTo</code> > <code>position</code>
623 * @param ch a <code>char</code> to search for
624 * @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
626 private static int scanForward(IDocument document, int position, String partitioning, int bound, char ch) {
627 return scanForward(document, position, partitioning, bound, new char[] {ch});
631 * Checks whether the content of <code>document</code> in the range (<code>offset</code>, <code>length</code>)
632 * contains the <code>new</code> keyword.
634 * @param document the document being modified
635 * @param offset the first character position in <code>document</code> to be considered
636 * @param length the length of the character range to be considered
637 * @param partitioning the document partitioning
638 * @return <code>true</code> if the specified character range contains a <code>new</code> keyword, <code>false</code> otherwise.
640 private static boolean isNewMatch(IDocument document, int offset, int length, String partitioning) {
641 Assert.isTrue(length >= 0);
642 Assert.isTrue(offset >= 0);
643 Assert.isTrue(offset + length < document.getLength() + 1);
646 String text= document.get(offset, length);
647 int pos= text.indexOf("new"); //$NON-NLS-1$
649 while (pos != -1 && !isDefaultPartition(document, pos + offset, partitioning))
650 pos= text.indexOf("new", pos + 2); //$NON-NLS-1$
655 if (pos != 0 && Character.isJavaIdentifierPart(text.charAt(pos - 1)))
658 if (pos + 3 < length && Character.isJavaIdentifierPart(text.charAt(pos + 3)))
663 } catch (BadLocationException e) {
669 * Checks whether the content of <code>document</code> at <code>position</code> looks like an
670 * anonymous class definition. <code>position</code> must be to the left of the opening
671 * parenthesis of the definition's parameter list.
673 * @param document the document being modified
674 * @param position the first character position in <code>document</code> to be considered
675 * @param partitioning the document partitioning
676 * @return <code>true</code> if the content of <code>document</code> looks like an anonymous class definition, <code>false</code> otherwise
678 private static boolean looksLikeAnonymousClassDef(IDocument document, int position, String partitioning) {
679 int previousCommaParenEqual= scanBackward(document, position - 1, partitioning, -1, new char[] {',', '(', '='});
680 if (previousCommaParenEqual == -1 || position < previousCommaParenEqual + 5) // 2 for borders, 3 for "new"
683 if (isNewMatch(document, previousCommaParenEqual + 1, position - previousCommaParenEqual - 2, partitioning))
690 * Checks whether <code>position</code> resides in a default (Java) partition of <code>document</code>.
692 * @param document the document being modified
693 * @param position the position to be checked
694 * @param partitioning the document partitioning
695 * @return <code>true</code> if <code>position</code> is in the default partition of <code>document</code>, <code>false</code> otherwise
697 private static boolean isDefaultPartition(IDocument document, int position, String partitioning) {
698 Assert.isTrue(position >= 0);
699 Assert.isTrue(position <= document.getLength());
702 // don't use getPartition2 since we're interested in the scanned character's partition
703 ITypedRegion region= TextUtilities.getPartition(document, partitioning, position, false);
704 return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
706 } catch (BadLocationException e) {
713 * Finds the position of the parenthesis matching the closing parenthesis at <code>position</code>.
715 * @param document the document being modified
716 * @param position the position in <code>document</code> of a closing parenthesis
717 * @param partitioning the document partitioning
718 * @return the position in <code>document</code> of the matching parenthesis, or -1 if none can be found
720 private static int findOpeningParenMatch(IDocument document, int position, String partitioning) {
721 final char CLOSING_PAREN= ')';
722 final char OPENING_PAREN= '(';
724 Assert.isTrue(position < document.getLength());
725 Assert.isTrue(position >= 0);
726 Assert.isTrue(isDefaultPartition(document, position, partitioning));
730 Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
734 position= scanBackward(document, position - 1, partitioning, -1, new char[] {CLOSING_PAREN, OPENING_PAREN});
738 if (document.getChar(position) == CLOSING_PAREN)
747 } catch (BadLocationException e) {
753 * Checks whether, to the left of <code>position</code> and separated only by whitespace,
754 * <code>document</code> contains a keyword taking a parameter list and a block after it.
755 * These are: <code>if</code>, <code>while</code>, <code>catch</code>, <code>for</code>, <code>synchronized</code>, <code>switch</code>.
757 * @param document the document being modified
758 * @param position the first character position in <code>document</code> to be considered
759 * @param partitioning the document partitioning
760 * @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
762 private static boolean looksLikeIfWhileForCatch(IDocument document, int position, String partitioning) {
763 position= firstNonWhitespaceBackward(document, position, partitioning, -1);
767 return looksLike(document, position, "if") //$NON-NLS-1$
768 || looksLike(document, position, "while") //$NON-NLS-1$
769 || looksLike(document, position, "catch") //$NON-NLS-1$
770 || looksLike(document, position, "synchronized") //$NON-NLS-1$
771 || looksLike(document, position, "switch") //$NON-NLS-1$
772 || looksLike(document, position, "for"); //$NON-NLS-1$
776 * Checks whether code>document</code> contains the <code>String</code> <code>like</code> such
777 * that its last character is at <code>position</code>. If <code>like</code> starts with a
778 * identifier part (as determined by {@link Character#isJavaIdentifierPart(char)}), it is also made
779 * sure that <code>like</code> is preceded by some non-identifier character or stands at the
782 * @param document the document being modified
783 * @param position the first character position in <code>document</code> to be considered
784 * @param like the <code>String</code> to look for.
785 * @return <code>true</code> if <code>document</code> contains <code>like</code> such that it ends at <code>position</code>, <code>false</code> otherwise
787 private static boolean looksLike(IDocument document, int position, String like) {
788 int length= like.length();
789 if (position < length - 1)
793 if (!like.equals(document.get(position - length + 1, length)))
796 if (position >= length && Character.isJavaIdentifierPart(like.charAt(0)) && Character.isJavaIdentifierPart(document.getChar(position - length)))
799 } catch (BadLocationException e) {
807 * Checks whether the content of <code>document</code> at <code>position</code> looks like a
808 * method declaration header (i.e. only the return type and method name). <code>position</code>
809 * must be just left of the opening parenthesis of the parameter list.
811 * @param document the document being modified
812 * @param position the first character position in <code>document</code> to be considered
813 * @param partitioning the document partitioning
814 * @return <code>true</code> if the content of <code>document</code> looks like a method definition, <code>false</code> otherwise
816 private static boolean looksLikeMethodDecl(IDocument document, int position, String partitioning) {
819 position= eatIdentToLeft(document, position, partitioning);
823 position= eatBrackets(document, position - 1, partitioning);
827 position= eatIdentToLeft(document, position - 1, partitioning);
829 return position != -1;
833 * From <code>position</code> to the left, eats any whitespace and then a pair of brackets
834 * as used to declare an array return type like <pre>String [ ]</pre>.
835 * The return value is either the position of the opening bracket or <code>position</code> if no
836 * pair of brackets can be parsed.
838 * @param document the document being modified
839 * @param position the first character position in <code>document</code> to be considered
840 * @param partitioning the document partitioning
841 * @return the smallest character position of bracket pair or <code>position</code>
843 private static int eatBrackets(IDocument document, int position, String partitioning) {
844 // accept array return type
845 int pos= firstNonWhitespaceBackward(document, position, partitioning, -1);
847 if (pos > 1 && document.getChar(pos) == ']') {
848 pos= firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
849 if (pos > 0 && document.getChar(pos) == '[')
852 } catch (BadLocationException e) {
859 * From <code>position</code> to the left, eats any whitespace and the first identifier, returning
860 * the position of the first identifier character (in normal read order).
861 * <p>When called on a document with content <code>" some string "</code> and positionition 13, the
862 * return value will be 6 (the first letter in <code>string</code>).
865 * @param document the document being modified
866 * @param position the first character position in <code>document</code> to be considered
867 * @param partitioning the document partitioning
868 * @return the smallest character position of an identifier or -1 if none can be found; always <= <code>position</code>
870 private static int eatIdentToLeft(IDocument document, int position, String partitioning) {
873 Assert.isTrue(position < document.getLength());
875 int p= firstNonWhitespaceBackward(document, position, partitioning, -1);
882 char ch= document.getChar(p);
883 if (Character.isJavaIdentifierPart(ch)) {
888 // length must be > 0
889 if (Character.isWhitespace(ch) && p != position)
896 // start of document reached
899 } catch (BadLocationException e) {
905 * Returns a position in the first java partition after the last non-empty and non-comment partition.
906 * There is no non-whitespace from the returned position to the end of the partition it is contained in.
908 * @param document the document being modified
909 * @param line the line under investigation
910 * @param offset the caret offset into <code>line</code>
911 * @param partitioning the document partitioning
912 * @return the position of the next Java partition, or the end of <code>line</code>
914 private static int nextPartitionOrLineEnd(IDocument document, ITextSelection line, int offset, String partitioning) {
915 // run relative to document
916 final int docOffset= offset + line.getOffset();
917 final int eol= line.getOffset() + line.getLength();
918 int nextPartitionPos= eol; // init with line end
919 int validPosition= docOffset;
922 ITypedRegion partition= TextUtilities.getPartition(document, partitioning, nextPartitionPos, true);
923 validPosition= getValidPositionForPartition(document, partition, eol);
924 while (validPosition == -1) {
925 nextPartitionPos= partition.getOffset() - 1;
926 if (nextPartitionPos < docOffset) {
927 validPosition= docOffset;
930 partition= TextUtilities.getPartition(document, partitioning, nextPartitionPos, false);
931 validPosition= getValidPositionForPartition(document, partition, eol);
933 } catch (BadLocationException e) {
936 validPosition= Math.max(validPosition, docOffset);
937 // make relative to line
938 validPosition -= line.getOffset();
939 return validPosition;
943 * Returns a valid insert location (except for whitespace) in <code>partition</code> or -1 if
944 * there is no valid insert location.
945 * An valid insert location is right after any java string or character partition, or at the end
946 * of a java default partition, but never behind <code>maxOffset</code>. Comment partitions or
947 * empty java partitions do never yield valid insert positions.
949 * @param doc the document being modified
950 * @param partition the current partition
951 * @param maxOffset the maximum offset to consider
952 * @return a valid insert location in <code>partition</code>, or -1 if there is no valid insert location
954 private static int getValidPositionForPartition(IDocument doc, ITypedRegion partition, int maxOffset) {
955 final int INVALID= -1;
957 if (IJavaPartitions.JAVA_DOC.equals(partition.getType()))
959 if (IJavaPartitions.JAVA_MULTI_LINE_COMMENT.equals(partition.getType()))
961 if (IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(partition.getType()))
964 int endOffset= Math.min(maxOffset, partition.getOffset() + partition.getLength());
966 if (IJavaPartitions.JAVA_CHARACTER.equals(partition.getType()))
968 if (IJavaPartitions.JAVA_STRING.equals(partition.getType()))
970 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
972 if (doc.get(partition.getOffset(), endOffset - partition.getOffset()).trim().length() == 0)
976 } catch (BadLocationException e) {
980 // default: we don't know anything about the partition - assume valid
985 * Determines whether the current line contains a for statement.
986 * Algorithm: any "for" word in the line is a positive, "for" contained in a string literal will
987 * produce a false positive.
989 * @param line the line where the change is being made
990 * @param offset the position of the caret
991 * @return <code>true</code> if <code>line</code> contains <code>for</code>, <code>false</code> otherwise
993 private static boolean isForStatement(String line, int offset) {
994 /* searching for (^|\s)for(\s|$) */
995 int forPos= line.indexOf("for"); //$NON-NLS-1$
997 if ((forPos == 0 || !Character.isJavaIdentifierPart(line.charAt(forPos - 1))) && (line.length() == forPos + 3 || !Character.isJavaIdentifierPart(line.charAt(forPos + 3))))
1004 * Returns the position in <code>text</code> after which there comes only whitespace, up to
1005 * <code>offset</code>.
1007 * @param text the text being searched
1008 * @param offset the maximum offset to search for
1009 * @return the smallest value <code>v</code> such that <code>text.substring(v, offset).trim() == 0</code>
1011 private static int startOfWhitespaceBeforeOffset(String text, int offset) {
1012 int i= Math.min(offset, text.length());
1013 for (; i >= 1; i--) {
1014 if (!Character.isWhitespace(text.charAt(i - 1)))