1 /*******************************************************************************
2 * Copyright (c) 2000, 2004 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Common Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/cpl-v10.html
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text;
13 import java.util.Arrays;
15 import net.sourceforge.phpdt.internal.compiler.parser.Scanner;
17 //import net.sourceforge.phpdt.internal.corext.Assert;
18 import org.eclipse.core.runtime.Assert;
19 import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
20 import net.sourceforge.phpdt.ui.PreferenceConstants;
21 //import net.sourceforge.phpeclipse.PHPeclipsePlugin;
22 import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
23 import net.sourceforge.phpeclipse.ui.WebUI;
25 import org.eclipse.jface.preference.IPreferenceStore;
26 import org.eclipse.jface.text.BadLocationException;
27 import org.eclipse.jface.text.DocumentCommand;
28 import org.eclipse.jface.text.IAutoEditStrategy;
29 import org.eclipse.jface.text.IDocument;
30 import org.eclipse.jface.text.IRegion;
31 import org.eclipse.jface.text.ITextSelection;
32 import org.eclipse.jface.text.ITypedRegion;
33 import org.eclipse.jface.text.Region;
34 import org.eclipse.jface.text.TextSelection;
35 import org.eclipse.jface.text.TextUtilities;
36 import org.eclipse.text.edits.DeleteEdit;
37 import org.eclipse.text.edits.MalformedTreeException;
38 import org.eclipse.text.edits.ReplaceEdit;
39 import org.eclipse.text.edits.TextEdit;
40 import org.eclipse.ui.IEditorPart;
41 import org.eclipse.ui.IWorkbenchPage;
42 import org.eclipse.ui.texteditor.ITextEditorExtension2;
43 import org.eclipse.ui.texteditor.ITextEditorExtension3;
46 * Modifies <code>DocumentCommand</code>s inserting semicolons and opening
47 * braces to place them smartly, i.e. moving them to the end of a line if that
48 * is what the user expects.
51 * In practice, semicolons and braces (and the caret) are moved to the end of
52 * the line if they are typed anywhere except for semicolons in a
53 * <code>for</code> statements definition. If the line contains a semicolon or
54 * brace after the current caret position, the cursor is moved after it.
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$
65 /** Char representation of a semicolon. */
66 private static final char SEMICHAR = ';';
68 /** String represenattion of a opening brace. */
69 private static final String BRACE = "{"; //$NON-NLS-1$
71 /** Char representation of a opening brace */
72 private static final char BRACECHAR = '{';
74 private char fCharacter;
76 private String fPartitioning;
79 * Creates a new SmartSemicolonAutoEditStrategy.
82 * the document partitioning
84 public SmartSemicolonAutoEditStrategy(String partitioning) {
85 fPartitioning = partitioning;
89 * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
90 * org.eclipse.jface.text.DocumentCommand)
92 public void customizeDocumentCommand(IDocument document,
93 DocumentCommand command) {
95 // also customize if <code>doit</code> is false (so it works in code
96 // completion situations)
100 if (command.text == null)
103 if (command.text.equals(SEMICOLON))
104 fCharacter = SEMICHAR;
105 else if (command.text.equals(BRACE))
106 fCharacter = BRACECHAR;
110 IPreferenceStore store = WebUI.getDefault()
111 .getPreferenceStore();
112 if (fCharacter == SEMICHAR
114 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
116 if (fCharacter == BRACECHAR
118 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
121 IWorkbenchPage page = WebUI.getActivePage();
124 IEditorPart part = page.getActiveEditor();
125 if (!(part instanceof PHPUnitEditor))
127 PHPUnitEditor editor = (PHPUnitEditor) part;
128 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
129 || !editor.isEditable())
131 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
132 .getAdapter(ITextEditorExtension2.class);
133 if (extension != null && !extension.validateEditorInputState())
135 if (isMultilineSelection(document, command))
138 // 1: find concerned line / position in java code, location in statement
139 int pos = command.offset;
142 IRegion l = document.getLineInformationOfOffset(pos);
143 line = new TextSelection(document, l.getOffset(), l.getLength());
144 } catch (BadLocationException e) {
148 // 2: choose action based on findings (is for-Statement?)
149 // for now: compute the best position to insert the new character
150 int positionInLine = computeCharacterPosition(document, line, pos
151 - line.getOffset(), fCharacter, fPartitioning);
152 int position = positionInLine + line.getOffset();
154 // never position before the current position!
158 // never double already existing content
159 if (alreadyPresent(document, fCharacter, position))
162 // don't do special processing if what we do is actually the normal
164 String insertion = adjustSpacing(document, position, fCharacter);
165 if (command.offset == position && insertion.equals(command.text))
170 final SmartBackspaceManager manager = (SmartBackspaceManager) editor
171 .getAdapter(SmartBackspaceManager.class);
173 && WebUI.getDefault().getPreferenceStore()
175 PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
176 TextEdit e1 = new ReplaceEdit(command.offset, command.text
177 .length(), document.get(command.offset, command.length));
178 UndoSpec s1 = new UndoSpec(command.offset
179 + command.text.length(), new Region(command.offset, 0),
180 new TextEdit[] { e1 }, 0, null);
182 DeleteEdit smart = new DeleteEdit(position, insertion.length());
183 ReplaceEdit raw = new ReplaceEdit(command.offset,
184 command.length, command.text);
185 UndoSpec s2 = new UndoSpec(position + insertion.length(),
186 new Region(command.offset + command.text.length(), 0),
187 new TextEdit[] { smart, raw }, 2, s1);
188 manager.register(s2);
192 command.offset = position;
194 command.caretOffset = position;
195 command.text = insertion;
197 command.owner = null;
198 } catch (MalformedTreeException e) {
200 } catch (BadLocationException e) {
207 * Returns <code>true</code> if the document command is applied on a multi
208 * line selection, <code>false</code> otherwise.
214 * @return <code>true</code> if <code>command</code> is a multiline
217 private boolean isMultilineSelection(IDocument document,
218 DocumentCommand command) {
220 return document.getNumberOfLines(command.offset, command.length) > 1;
221 } catch (BadLocationException e) {
228 * Adds a space before a brace if it is inserted after a parenthesis, equal
229 * sign, or one of the keywords <code>try, else, do</code>.
232 * the document we are working on
234 * the insert position of <code>character</code>
236 * the character to be inserted
237 * @return a <code>String</code> consisting of <code>character</code>
238 * plus any additional spacing
240 private String adjustSpacing(IDocument doc, int position, char character) {
241 if (character == BRACECHAR) {
242 if (position > 0 && position <= doc.getLength()) {
243 int pos = position - 1;
244 if (looksLike(doc, pos, ")") //$NON-NLS-1$
245 || looksLike(doc, pos, "=") //$NON-NLS-1$
246 || looksLike(doc, pos, "]") //$NON-NLS-1$
247 || looksLike(doc, pos, "try") //$NON-NLS-1$
248 || looksLike(doc, pos, "else") //$NON-NLS-1$
249 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
250 || looksLike(doc, pos, "static") //$NON-NLS-1$
251 || looksLike(doc, pos, "finally") //$NON-NLS-1$
252 || looksLike(doc, pos, "do")) //$NON-NLS-1$
253 return new String(new char[] { ' ', character });
257 return new String(new char[] { character });
261 * Checks whether a character to be inserted is already present at the
262 * insert location (perhaps separated by some whitespace from
263 * <code>position</code>.
266 * the document we are working on
268 * the insert position of <code>ch</code>
270 * the character to be inserted
271 * @return <code>true</code> if <code>ch</code> is already present at
272 * <code>location</code>, <code>false</code> otherwise
274 private boolean alreadyPresent(IDocument document, char ch, int position) {
275 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
276 document.getLength());
278 if (pos != -1 && document.getChar(pos) == ch)
280 } catch (BadLocationException e) {
287 * Computes the next insert position of the given character in the current
291 * the document we are working on
293 * the line where the change is being made
295 * the position of the caret in the line when
296 * <code>character</code> was typed
298 * the character to look for
299 * @param partitioning
300 * the document partitioning
301 * @return the position where <code>character</code> should be inserted /
304 protected static int computeCharacterPosition(IDocument document,
305 ITextSelection line, int offset, char character, String partitioning) {
306 String text = line.getText();
311 if (character == BRACECHAR) {
313 insertPos = computeArrayInitializationPos(document, line, offset,
316 if (insertPos == -1) {
317 insertPos = computeAfterTryDoElse(document, line, offset);
320 if (insertPos == -1) {
321 insertPos = computeAfterParenthesis(document, line, offset,
325 } else if (character == SEMICHAR) {
327 if (isForStatement(text, offset)) {
328 insertPos = -1; // don't do anything in for statements, as semis
329 // are vital part of these
331 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
332 offset, partitioning);
333 insertPos = startOfWhitespaceBeforeOffset(text,
335 // if there is a semi present, return its location as
336 // alreadyPresent() will take it out this way.
337 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
338 insertPos = insertPos - 1;
342 Assert.isTrue(false);
350 * Computes an insert position for an opening brace if <code>offset</code>
351 * maps to a position in <code>document</code> that looks like being the
352 * RHS of an assignment or like an array definition.
355 * the document being modified
357 * the current line under investigation
359 * the offset of the caret position, relative to the line start.
360 * @param partitioning
361 * the document partitioning
362 * @return an insert position relative to the line start if
363 * <code>line</code> looks like being an array initialization at
364 * <code>offset</code>, -1 otherwise
366 private static int computeArrayInitializationPos(IDocument document,
367 ITextSelection line, int offset, String partitioning) {
368 // search backward while WS, find = (not != <= >= ==) in default
370 int pos = offset + line.getOffset();
375 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
382 char ch = document.getChar(p);
383 if (ch != '=' && ch != ']')
389 p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
393 ch = document.getChar(p);
394 if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
397 } catch (BadLocationException e) {
403 * Computes an insert position for an opening brace if <code>offset</code>
404 * maps to a position in <code>document</code> involving a keyword taking
405 * a block after it. These are: <code>try</code>, <code>do</code>,
406 * <code>synchronized</code>, <code>static</code>,
407 * <code>finally</code>, or <code>else</code>.
410 * the document being modified
412 * the current line under investigation
414 * the offset of the caret position, relative to the line start.
415 * @return an insert position relative to the line start if
416 * <code>line</code> contains one of the above keywords at or
417 * before <code>offset</code>, -1 otherwise
419 private static int computeAfterTryDoElse(IDocument doc,
420 ITextSelection line, int offset) {
421 // search backward while WS, find 'try', 'do', 'else' in default
423 int p = offset + line.getOffset();
424 p = firstWhitespaceToRight(doc, p);
429 if (looksLike(doc, p, "try") //$NON-NLS-1$
430 || looksLike(doc, p, "do") //$NON-NLS-1$
431 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
432 || looksLike(doc, p, "static") //$NON-NLS-1$
433 || looksLike(doc, p, "finally") //$NON-NLS-1$
434 || looksLike(doc, p, "else")) //$NON-NLS-1$
435 return p + 1 - line.getOffset();
441 * Computes an insert position for an opening brace if <code>offset</code>
442 * maps to a position in <code>document</code> with a expression in
443 * parenthesis that will take a block after the closing parenthesis.
446 * the document being modified
448 * the current line under investigation
450 * the offset of the caret position, relative to the line start.
451 * @param partitioning
452 * the document partitioning
453 * @return an insert position relative to the line start if
454 * <code>line</code> contains a parenthesized expression that can
455 * be followed by a block, -1 otherwise
457 private static int computeAfterParenthesis(IDocument document,
458 ITextSelection line, int offset, String partitioning) {
459 // find the opening parenthesis for every closing parenthesis on the
460 // current line after offset
461 // return the position behind the closing parenthesis if it looks like a
462 // method declaration
463 // or an expression for an if, while, for, catch statement
464 int pos = offset + line.getOffset();
465 int length = line.getOffset() + line.getLength();
466 int scanTo = scanForward(document, pos, partitioning, length, '}');
470 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
473 int startScan = closingParen + 1;
474 closingParen = scanForward(document, startScan, partitioning,
476 if (closingParen == -1)
479 int openingParen = findOpeningParenMatch(document, closingParen,
482 // no way an expression at the beginning of the document can mean
484 if (openingParen < 1)
487 // only select insert positions for parenthesis currently embracing
489 if (openingParen > pos)
492 if (looksLikeAnonymousClassDef(document, openingParen - 1,
494 return closingParen + 1 - line.getOffset();
496 if (looksLikeIfWhileForCatch(document, openingParen - 1,
498 return closingParen + 1 - line.getOffset();
500 if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
501 return closingParen + 1 - line.getOffset();
509 * Finds a closing parenthesis to the left of <code>position</code> in
510 * document, where that parenthesis is only separated by whitespace from
511 * <code>position</code>. If no such parenthesis can be found,
512 * <code>position</code> is returned.
515 * the document being modified
517 * the first character position in <code>document</code> to be
519 * @param partitioning
520 * the document partitioning
521 * @return the position of a closing parenthesis left to
522 * <code>position</code> separated only by whitespace, or
523 * <code>position</code> if no parenthesis can be found
525 private static int findClosingParenToLeft(IDocument document, int position,
526 String partitioning) {
527 final char CLOSING_PAREN = ')';
532 int nonWS = firstNonWhitespaceBackward(document, position - 1,
534 if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
536 } catch (BadLocationException e1) {
542 * Finds the first whitespace character position to the right of (and
543 * including) <code>position</code>.
546 * the document being modified
548 * the first character position in <code>document</code> to be
550 * @return the position of a whitespace character greater or equal than
551 * <code>position</code> separated only by whitespace, or -1 if
554 private static int firstWhitespaceToRight(IDocument document, int position) {
555 int length = document.getLength();
556 Assert.isTrue(position >= 0);
557 Assert.isTrue(position <= length);
560 while (position < length) {
561 char ch = document.getChar(position);
562 if (Character.isWhitespace(ch))
567 } catch (BadLocationException e) {
573 * Finds the highest position in <code>document</code> such that the
574 * position is <= <code>position</code> and > <code>bound</code>
575 * and <code>Character.isWhitespace(document.getChar(pos))</code>
576 * evaluates to <code>false</code> and the position is in the default
580 * the document being modified
582 * the first character position in <code>document</code> to be
584 * @param partitioning
585 * the document partitioning
587 * the first position in <code>document</code> to not consider
588 * any more, with <code>bound</code> > <code>position</code>
589 * @return the highest position of one element in <code>chars</code> in [<code>position</code>,
590 * <code>scanTo</code>) that resides in a Java partition, or
591 * <code>-1</code> if none can be found
593 private static int firstNonWhitespaceBackward(IDocument document,
594 int position, String partitioning, int bound) {
595 Assert.isTrue(position < document.getLength());
596 Assert.isTrue(bound >= -1);
599 while (position > bound) {
600 char ch = document.getChar(position);
601 if (!Character.isWhitespace(ch)
602 && isDefaultPartition(document, position, partitioning))
606 } catch (BadLocationException e) {
612 * Finds the smallest position in <code>document</code> such that the
613 * position is >= <code>position</code> and < <code>bound</code>
614 * and <code>Character.isWhitespace(document.getChar(pos))</code>
615 * evaluates to <code>false</code> and the position is in the default
619 * the document being modified
621 * the first character position in <code>document</code> to be
623 * @param partitioning
624 * the document partitioning
626 * the first position in <code>document</code> to not consider
627 * any more, with <code>bound</code> > <code>position</code>
628 * @return the smallest position of one element in <code>chars</code> in [<code>position</code>,
629 * <code>scanTo</code>) that resides in a Java partition, or
630 * <code>-1</code> if none can be found
632 private static int firstNonWhitespaceForward(IDocument document,
633 int position, String partitioning, int bound) {
634 Assert.isTrue(position >= 0);
635 Assert.isTrue(bound <= document.getLength());
638 while (position < bound) {
639 char ch = document.getChar(position);
640 if (!Character.isWhitespace(ch)
641 && isDefaultPartition(document, position, partitioning))
645 } catch (BadLocationException e) {
651 * Finds the highest position in <code>document</code> such that the
652 * position is <= <code>position</code> and > <code>bound</code>
653 * and <code>document.getChar(position) == ch</code> evaluates to
654 * <code>true</code> for at least one ch in <code>chars</code> and the
655 * position is in the default partition.
658 * the document being modified
660 * the first character position in <code>document</code> to be
662 * @param partitioning
663 * the document partitioning
665 * the first position in <code>document</code> to not consider
666 * any more, with <code>scanTo</code> >
667 * <code>position</code>
669 * an array of <code>char</code> to search for
670 * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
671 * <code>position</code>] that resides in a Java partition, or
672 * <code>-1</code> if none can be found
674 private static int scanBackward(IDocument document, int position,
675 String partitioning, int bound, char[] chars) {
676 Assert.isTrue(bound >= -1);
677 Assert.isTrue(position < document.getLength());
682 while (position > bound) {
684 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
685 && isDefaultPartition(document, position, partitioning))
690 } catch (BadLocationException e) {
696 // * Finds the highest position in <code>document</code> such that the
697 // position is <= <code>position</code>
698 // * and > <code>bound</code> and <code>document.getChar(position) ==
699 // ch</code> evaluates to <code>true</code>
700 // * and the position is in the default partition.
702 // * @param document the document being modified
703 // * @param position the first character position in <code>document</code>
705 // * @param bound the first position in <code>document</code> to not
706 // consider any more, with <code>scanTo</code> > <code>position</code>
707 // * @param chars an array of <code>char</code> to search for
708 // * @return the highest position of one element in <code>chars</code> in
709 // [<code>position</code>, <code>scanTo</code>) that resides in a Java
710 // partition, or <code>-1</code> if none can be found
712 // private static int scanBackward(IDocument document, int position, int
714 // return scanBackward(document, position, bound, new char[] {ch});
718 * Finds the lowest position in <code>document</code> such that the
719 * position is >= <code>position</code> and < <code>bound</code>
720 * and <code>document.getChar(position) == ch</code> evaluates to
721 * <code>true</code> for at least one ch in <code>chars</code> and the
722 * position is in the default partition.
725 * the document being modified
727 * the first character position in <code>document</code> to be
729 * @param partitioning
730 * the document partitioning
732 * the first position in <code>document</code> to not consider
733 * any more, with <code>scanTo</code> >
734 * <code>position</code>
736 * an array of <code>char</code> to search for
737 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
738 * <code>bound</code>) that resides in a Java partition, or
739 * <code>-1</code> if none can be found
741 private static int scanForward(IDocument document, int position,
742 String partitioning, int bound, char[] chars) {
743 Assert.isTrue(position >= 0);
744 Assert.isTrue(bound <= document.getLength());
749 while (position < bound) {
751 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
752 && isDefaultPartition(document, position, partitioning))
757 } catch (BadLocationException e) {
763 * Finds the lowest position in <code>document</code> such that the
764 * position is >= <code>position</code> and < <code>bound</code>
765 * and <code>document.getChar(position) == ch</code> evaluates to
766 * <code>true</code> and the position is in the default partition.
769 * the document being modified
771 * the first character position in <code>document</code> to be
773 * @param partitioning
774 * the document partitioning
776 * the first position in <code>document</code> to not consider
777 * any more, with <code>scanTo</code> >
778 * <code>position</code>
780 * an array of <code>char</code> to search for
781 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
782 * <code>bound</code>) that resides in a Java partition, or
783 * <code>-1</code> if none can be found
785 private static int scanForward(IDocument document, int position,
786 String partitioning, int bound, char ch) {
787 return scanForward(document, position, partitioning, bound,
792 * Checks whether the content of <code>document</code> in the range (<code>offset</code>,
793 * <code>length</code>) contains the <code>new</code> keyword.
796 * the document being modified
798 * the first character position in <code>document</code> to be
801 * the length of the character range to be considered
802 * @param partitioning
803 * the document partitioning
804 * @return <code>true</code> if the specified character range contains a
805 * <code>new</code> keyword, <code>false</code> otherwise.
807 private static boolean isNewMatch(IDocument document, int offset,
808 int length, String partitioning) {
809 Assert.isTrue(length >= 0);
810 Assert.isTrue(offset >= 0);
811 Assert.isTrue(offset + length < document.getLength() + 1);
814 String text = document.get(offset, length);
815 int pos = text.indexOf("new"); //$NON-NLS-1$
818 && !isDefaultPartition(document, pos + offset, partitioning))
819 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
824 if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
828 && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
833 } catch (BadLocationException e) {
839 * Checks whether the content of <code>document</code> at
840 * <code>position</code> looks like an anonymous class definition.
841 * <code>position</code> must be to the left of the opening parenthesis of
842 * the definition's parameter list.
845 * the document being modified
847 * the first character position in <code>document</code> to be
849 * @param partitioning
850 * the document partitioning
851 * @return <code>true</code> if the content of <code>document</code>
852 * looks like an anonymous class definition, <code>false</code>
855 private static boolean looksLikeAnonymousClassDef(IDocument document,
856 int position, String partitioning) {
857 int previousCommaOrParen = scanBackward(document, position - 1,
858 partitioning, -1, new char[] { ',', '(' });
859 if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2
867 if (isNewMatch(document, previousCommaOrParen + 1, position
868 - previousCommaOrParen - 2, partitioning))
875 * Checks whether <code>position</code> resides in a default (Java)
876 * partition of <code>document</code>.
879 * the document being modified
881 * the position to be checked
882 * @param partitioning
883 * the document partitioning
884 * @return <code>true</code> if <code>position</code> is in the default
885 * partition of <code>document</code>, <code>false</code>
888 private static boolean isDefaultPartition(IDocument document, int position,
889 String partitioning) {
890 Assert.isTrue(position >= 0);
891 Assert.isTrue(position <= document.getLength());
894 // don't use getPartition2 since we're interested in the scanned
895 // character's partition
896 ITypedRegion region = TextUtilities.getPartition(document,
897 partitioning, position, false);
898 return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
900 } catch (BadLocationException e) {
907 * Finds the position of the parenthesis matching the closing parenthesis at
908 * <code>position</code>.
911 * the document being modified
913 * the position in <code>document</code> of a closing
915 * @param partitioning
916 * the document partitioning
917 * @return the position in <code>document</code> of the matching
918 * parenthesis, or -1 if none can be found
920 private static int findOpeningParenMatch(IDocument document, int position,
921 String partitioning) {
922 final char CLOSING_PAREN = ')';
923 final char OPENING_PAREN = '(';
925 Assert.isTrue(position < document.getLength());
926 Assert.isTrue(position >= 0);
927 Assert.isTrue(isDefaultPartition(document, position, partitioning));
931 Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
935 position = scanBackward(document, position - 1, partitioning,
936 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
940 if (document.getChar(position) == CLOSING_PAREN)
949 } catch (BadLocationException e) {
955 * Checks whether, to the left of <code>position</code> and separated only
956 * by whitespace, <code>document</code> contains a keyword taking a
957 * parameter list and a block after it. These are: <code>if</code>,
958 * <code>while</code>, <code>catch</code>, <code>for</code>,
959 * <code>synchronized</code>, <code>switch</code>.
962 * the document being modified
964 * the first character position in <code>document</code> to be
966 * @param partitioning
967 * the document partitioning
968 * @return <code>true</code> if <code>document</code> contains any of
969 * the above keywords to the left of <code>position</code>,
970 * <code>false</code> otherwise
972 private static boolean looksLikeIfWhileForCatch(IDocument document,
973 int position, String partitioning) {
974 position = firstNonWhitespaceBackward(document, position, partitioning,
979 return looksLike(document, position, "if") //$NON-NLS-1$
980 || looksLike(document, position, "while") //$NON-NLS-1$
981 || looksLike(document, position, "catch") //$NON-NLS-1$
982 || looksLike(document, position, "synchronized") //$NON-NLS-1$
983 || looksLike(document, position, "switch") //$NON-NLS-1$
984 || looksLike(document, position, "for"); //$NON-NLS-1$
988 * Checks whether code>document</code> contains the <code>String</code> <code>like</code>
989 * such that its last character is at <code>position</code>. If <code>like</code>
990 * starts with a identifier part (as determined by
991 * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that
992 * <code>like</code> is preceded by some non-identifier character or
993 * stands at the document start.
996 * the document being modified
998 * the first character position in <code>document</code> to be
1001 * the <code>String</code> to look for.
1002 * @return <code>true</code> if <code>document</code> contains <code>like</code>
1003 * such that it ends at <code>position</code>, <code>false</code>
1006 private static boolean looksLike(IDocument document, int position,
1008 int length = like.length();
1009 if (position < length - 1)
1013 if (!like.equals(document.get(position - length + 1, length)))
1016 if (position >= length
1017 && Scanner.isPHPIdentifierPart(like.charAt(0))
1018 && Scanner.isPHPIdentifierPart(document.getChar(position
1022 } catch (BadLocationException e) {
1030 * Checks whether the content of <code>document</code> at
1031 * <code>position</code> looks like a method declaration header (i.e. only
1032 * the return type and method name). <code>position</code> must be just
1033 * left of the opening parenthesis of the parameter list.
1036 * the document being modified
1038 * the first character position in <code>document</code> to be
1040 * @param partitioning
1041 * the document partitioning
1042 * @return <code>true</code> if the content of <code>document</code>
1043 * looks like a method definition, <code>false</code> otherwise
1045 private static boolean looksLikeMethodDecl(IDocument document,
1046 int position, String partitioning) {
1049 position = eatIdentToLeft(document, position, partitioning);
1053 position = eatBrackets(document, position - 1, partitioning);
1057 position = eatIdentToLeft(document, position - 1, partitioning);
1059 return position != -1;
1063 * From <code>position</code> to the left, eats any whitespace and then a
1064 * pair of brackets as used to declare an array return type like
1068 * </pre>. The return value is either the position of the opening bracket
1069 * or <code>position</code> if no pair of brackets can be parsed.
1072 * the document being modified
1074 * the first character position in <code>document</code> to be
1076 * @param partitioning
1077 * the document partitioning
1078 * @return the smallest character position of bracket pair or
1079 * <code>position</code>
1081 private static int eatBrackets(IDocument document, int position,
1082 String partitioning) {
1083 // accept array return type
1084 int pos = firstNonWhitespaceBackward(document, position, partitioning,
1087 if (pos > 1 && document.getChar(pos) == ']') {
1088 pos = firstNonWhitespaceBackward(document, pos - 1,
1090 if (pos > 0 && document.getChar(pos) == '[')
1093 } catch (BadLocationException e) {
1100 * From <code>position</code> to the left, eats any whitespace and the
1101 * first identifier, returning the position of the first identifier
1102 * character (in normal read order).
1104 * When called on a document with content <code>" some string "</code> and
1105 * positionition 13, the return value will be 6 (the first letter in
1106 * <code>string</code>).
1110 * the document being modified
1112 * the first character position in <code>document</code> to be
1114 * @param partitioning
1115 * the document partitioning
1116 * @return the smallest character position of an identifier or -1 if none
1117 * can be found; always <= <code>position</code>
1119 private static int eatIdentToLeft(IDocument document, int position,
1120 String partitioning) {
1123 Assert.isTrue(position < document.getLength());
1125 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
1132 char ch = document.getChar(p);
1133 if (Scanner.isPHPIdentifierPart(ch)) {
1138 // length must be > 0
1139 if (Character.isWhitespace(ch) && p != position)
1146 // start of document reached
1149 } catch (BadLocationException e) {
1155 * Returns a position in the first java partition after the last non-empty
1156 * and non-comment partition. There is no non-whitespace from the returned
1157 * position to the end of the partition it is contained in.
1160 * the document being modified
1162 * the line under investigation
1164 * the caret offset into <code>line</code>
1165 * @param partitioning
1166 * the document partitioning
1167 * @return the position of the next Java partition, or the end of
1170 private static int nextPartitionOrLineEnd(IDocument document,
1171 ITextSelection line, int offset, String partitioning) {
1172 // run relative to document
1173 final int docOffset = offset + line.getOffset();
1174 final int eol = line.getOffset() + line.getLength();
1175 int nextPartitionPos = eol; // init with line end
1176 int validPosition = docOffset;
1179 ITypedRegion partition = TextUtilities.getPartition(document,
1180 partitioning, nextPartitionPos, true);
1181 validPosition = getValidPositionForPartition(document, partition,
1183 while (validPosition == -1) {
1184 nextPartitionPos = partition.getOffset() - 1;
1185 if (nextPartitionPos < docOffset) {
1186 validPosition = docOffset;
1189 partition = TextUtilities.getPartition(document, partitioning,
1190 nextPartitionPos, false);
1191 validPosition = getValidPositionForPartition(document,
1194 } catch (BadLocationException e) {
1197 validPosition = Math.max(validPosition, docOffset);
1198 // make relative to line
1199 validPosition -= line.getOffset();
1200 return validPosition;
1204 * Returns a valid insert location (except for whitespace) in
1205 * <code>partition</code> or -1 if there is no valid insert location. An
1206 * valid insert location is right after any java string or character
1207 * partition, or at the end of a java default partition, but never behind
1208 * <code>maxOffset</code>. Comment partitions or empty java partitions do
1209 * never yield valid insert positions.
1212 * the document being modified
1214 * the current partition
1216 * the maximum offset to consider
1217 * @return a valid insert location in <code>partition</code>, or -1 if
1218 * there is no valid insert location
1220 private static int getValidPositionForPartition(IDocument doc,
1221 ITypedRegion partition, int maxOffset) {
1222 final int INVALID = -1;
1224 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
1226 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
1228 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
1231 int endOffset = Math.min(maxOffset, partition.getOffset()
1232 + partition.getLength());
1234 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
1235 // return endOffset;
1236 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
1238 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
1240 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
1242 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1244 if (doc.get(partition.getOffset(),
1245 endOffset - partition.getOffset()).trim().length() == 0)
1249 } catch (BadLocationException e) {
1253 // default: we don't know anything about the partition - assume valid
1258 * Determines whether the current line contains a for statement. Algorithm:
1259 * any "for" word in the line is a positive, "for" contained in a string
1260 * literal will produce a false positive.
1263 * the line where the change is being made
1265 * the position of the caret
1266 * @return <code>true</code> if <code>line</code> contains
1267 * <code>for</code>, <code>false</code> otherwise
1269 private static boolean isForStatement(String line, int offset) {
1270 /* searching for (^|\s)for(\s|$) */
1271 int forPos = line.indexOf("for"); //$NON-NLS-1$
1273 if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
1274 .charAt(forPos - 1)))
1275 && (line.length() == forPos + 3 || !Scanner
1276 .isPHPIdentifierPart(line.charAt(forPos + 3))))
1283 * Returns the position in <code>text</code> after which there comes only
1284 * whitespace, up to <code>offset</code>.
1287 * the text being searched
1289 * the maximum offset to search for
1290 * @return the smallest value <code>v</code> such that
1291 * <code>text.substring(v, offset).trim() == 0</code>
1293 private static int startOfWhitespaceBeforeOffset(String text, int offset) {
1294 int i = Math.min(offset, text.length());
1295 for (; i >= 1; i--) {
1296 if (!Character.isWhitespace(text.charAt(i - 1)))