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;
24 import org.eclipse.jface.preference.IPreferenceStore;
25 import org.eclipse.jface.text.BadLocationException;
26 import org.eclipse.jface.text.DocumentCommand;
27 import org.eclipse.jface.text.IAutoEditStrategy;
28 import org.eclipse.jface.text.IDocument;
29 import org.eclipse.jface.text.IRegion;
30 import org.eclipse.jface.text.ITextSelection;
31 import org.eclipse.jface.text.ITypedRegion;
32 import org.eclipse.jface.text.Region;
33 import org.eclipse.jface.text.TextSelection;
34 import org.eclipse.jface.text.TextUtilities;
35 import org.eclipse.text.edits.DeleteEdit;
36 import org.eclipse.text.edits.MalformedTreeException;
37 import org.eclipse.text.edits.ReplaceEdit;
38 import org.eclipse.text.edits.TextEdit;
39 import org.eclipse.ui.IEditorPart;
40 import org.eclipse.ui.IWorkbenchPage;
41 import org.eclipse.ui.texteditor.ITextEditorExtension2;
42 import org.eclipse.ui.texteditor.ITextEditorExtension3;
45 * Modifies <code>DocumentCommand</code>s inserting semicolons and opening
46 * braces to place them smartly, i.e. moving them to the end of a line if that
47 * is what the user expects.
50 * In practice, semicolons and braces (and the caret) are moved to the end of
51 * the line if they are typed anywhere except for semicolons in a
52 * <code>for</code> statements definition. If the line contains a semicolon or
53 * brace after the current caret position, the cursor is moved after it.
56 * @see org.eclipse.jface.text.DocumentCommand
59 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
61 /** String representation of a semicolon. */
62 private static final String SEMICOLON = ";"; //$NON-NLS-1$
64 /** Char representation of a semicolon. */
65 private static final char SEMICHAR = ';';
67 /** String representation of a opening brace. */
68 private static final String BRACE = "{"; //$NON-NLS-1$
70 /** Char representation of a opening brace */
71 private static final char BRACECHAR = '{';
73 private char fCharacter;
75 private String fPartitioning;
78 * Creates a new SmartSemicolonAutoEditStrategy.
81 * the document partitioning
83 public SmartSemicolonAutoEditStrategy(String partitioning) {
84 fPartitioning = partitioning;
88 * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
89 * org.eclipse.jface.text.DocumentCommand)
91 public void customizeDocumentCommand(IDocument document,
92 DocumentCommand command) {
94 // also customize if <code>doit</code> is false (so it works in code
95 // completion situations)
99 if (command.text == null)
102 if (command.text.equals(SEMICOLON))
103 fCharacter = SEMICHAR;
104 else if (command.text.equals(BRACE))
105 fCharacter = BRACECHAR;
109 IPreferenceStore store = PHPeclipsePlugin.getDefault()
110 .getPreferenceStore();
111 if (fCharacter == SEMICHAR
113 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
115 if (fCharacter == BRACECHAR
117 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
120 IWorkbenchPage page = PHPeclipsePlugin.getActivePage();
123 IEditorPart part = page.getActiveEditor();
124 if (!(part instanceof PHPUnitEditor))
126 PHPUnitEditor editor = (PHPUnitEditor) part;
127 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
128 || !editor.isEditable())
130 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
131 .getAdapter(ITextEditorExtension2.class);
132 if (extension != null && !extension.validateEditorInputState())
134 if (isMultilineSelection(document, command))
137 // 1: find concerned line / position in java code, location in statement
138 int pos = command.offset;
141 IRegion l = document.getLineInformationOfOffset(pos);
142 line = new TextSelection(document, l.getOffset(), l.getLength());
143 } catch (BadLocationException e) {
147 // 2: choose action based on findings (is for-Statement?)
148 // for now: compute the best position to insert the new character
149 int positionInLine = computeCharacterPosition(document, line, pos
150 - line.getOffset(), fCharacter, fPartitioning);
151 int position = positionInLine + line.getOffset();
153 // never position before the current position!
157 // never double already existing content
158 if (alreadyPresent(document, fCharacter, position))
161 // don't do special processing if what we do is actually the normal
163 String insertion = adjustSpacing(document, position, fCharacter);
164 if (command.offset == position && insertion.equals(command.text))
169 final SmartBackspaceManager manager = (SmartBackspaceManager) editor
170 .getAdapter(SmartBackspaceManager.class);
172 && PHPeclipsePlugin.getDefault().getPreferenceStore()
174 PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
175 TextEdit e1 = new ReplaceEdit(command.offset, command.text
176 .length(), document.get(command.offset, command.length));
177 UndoSpec s1 = new UndoSpec(command.offset
178 + command.text.length(), new Region(command.offset, 0),
179 new TextEdit[] { e1 }, 0, null);
181 DeleteEdit smart = new DeleteEdit(position, insertion.length());
182 ReplaceEdit raw = new ReplaceEdit(command.offset,
183 command.length, command.text);
184 UndoSpec s2 = new UndoSpec(position + insertion.length(),
185 new Region(command.offset + command.text.length(), 0),
186 new TextEdit[] { smart, raw }, 2, s1);
187 manager.register(s2);
191 command.offset = position;
193 command.caretOffset = position;
194 command.text = insertion;
196 command.owner = null;
197 } catch (MalformedTreeException e) {
198 PHPeclipsePlugin.log(e);
199 } catch (BadLocationException e) {
200 PHPeclipsePlugin.log(e);
206 * Returns <code>true</code> if the document command is applied on a multi
207 * line selection, <code>false</code> otherwise.
213 * @return <code>true</code> if <code>command</code> is a multiline
216 private boolean isMultilineSelection(IDocument document,
217 DocumentCommand command) {
219 return document.getNumberOfLines(command.offset, command.length) > 1;
220 } catch (BadLocationException e) {
227 * Adds a space before a brace if it is inserted after a parenthesis, equal
228 * sign, or one of the keywords <code>try, else, do</code>.
231 * the document we are working on
233 * the insert position of <code>character</code>
235 * the character to be inserted
236 * @return a <code>String</code> consisting of <code>character</code>
237 * plus any additional spacing
239 private String adjustSpacing(IDocument doc, int position, char character) {
240 if (character == BRACECHAR) {
241 if (position > 0 && position <= doc.getLength()) {
242 int pos = position - 1;
243 if (looksLike(doc, pos, ")") //$NON-NLS-1$
244 || looksLike(doc, pos, "=") //$NON-NLS-1$
245 || looksLike(doc, pos, "]") //$NON-NLS-1$
246 || looksLike(doc, pos, "try") //$NON-NLS-1$
247 || looksLike(doc, pos, "else") //$NON-NLS-1$
248 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
249 || looksLike(doc, pos, "static") //$NON-NLS-1$
250 || looksLike(doc, pos, "finally") //$NON-NLS-1$
251 || looksLike(doc, pos, "do")) //$NON-NLS-1$
252 return new String(new char[] { ' ', character });
256 return new String(new char[] { character });
260 * Checks whether a character to be inserted is already present at the
261 * insert location (perhaps separated by some whitespace from
262 * <code>position</code>.
265 * the document we are working on
267 * the insert position of <code>ch</code>
269 * the character to be inserted
270 * @return <code>true</code> if <code>ch</code> is already present at
271 * <code>location</code>, <code>false</code> otherwise
273 private boolean alreadyPresent(IDocument document, char ch, int position) {
274 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
275 document.getLength());
277 if (pos != -1 && document.getChar(pos) == ch)
279 } catch (BadLocationException e) {
286 * Computes the next insert position of the given character in the current
290 * the document we are working on
292 * the line where the change is being made
294 * the position of the caret in the line when
295 * <code>character</code> was typed
297 * the character to look for
298 * @param partitioning
299 * the document partitioning
300 * @return the position where <code>character</code> should be inserted /
303 protected static int computeCharacterPosition(IDocument document,
304 ITextSelection line, int offset, char character, String partitioning) {
305 String text = line.getText();
310 if (character == BRACECHAR) {
312 insertPos = computeArrayInitializationPos(document, line, offset,
315 if (insertPos == -1) {
316 insertPos = computeAfterTryDoElse(document, line, offset);
319 if (insertPos == -1) {
320 insertPos = computeAfterParenthesis(document, line, offset,
324 } else if (character == SEMICHAR) {
326 if (isForStatement(text, offset)) {
327 insertPos = -1; // don't do anything in for statements, as semis
328 // are vital part of these
330 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
331 offset, partitioning);
332 insertPos = startOfWhitespaceBeforeOffset(text,
334 // if there is a semi present, return its location as
335 // alreadyPresent() will take it out this way.
336 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
337 insertPos = insertPos - 1;
341 Assert.isTrue(false);
349 * Computes an insert position for an opening brace if <code>offset</code>
350 * maps to a position in <code>document</code> that looks like being the
351 * RHS of an assignment or like an array definition.
354 * the document being modified
356 * the current line under investigation
358 * the offset of the caret position, relative to the line start.
359 * @param partitioning
360 * the document partitioning
361 * @return an insert position relative to the line start if
362 * <code>line</code> looks like being an array initialization at
363 * <code>offset</code>, -1 otherwise
365 private static int computeArrayInitializationPos(IDocument document,
366 ITextSelection line, int offset, String partitioning) {
367 // search backward while WS, find = (not != <= >= ==) in default
369 int pos = offset + line.getOffset();
374 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
381 char ch = document.getChar(p);
382 if (ch != '=' && ch != ']')
388 p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
392 ch = document.getChar(p);
393 if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
396 } catch (BadLocationException e) {
402 * Computes an insert position for an opening brace if <code>offset</code>
403 * maps to a position in <code>document</code> involving a keyword taking
404 * a block after it. These are: <code>try</code>, <code>do</code>,
405 * <code>synchronized</code>, <code>static</code>,
406 * <code>finally</code>, or <code>else</code>.
409 * the document being modified
411 * the current line under investigation
413 * the offset of the caret position, relative to the line start.
414 * @return an insert position relative to the line start if
415 * <code>line</code> contains one of the above keywords at or
416 * before <code>offset</code>, -1 otherwise
418 private static int computeAfterTryDoElse(IDocument doc,
419 ITextSelection line, int offset) {
420 // search backward while WS, find 'try', 'do', 'else' in default
422 int p = offset + line.getOffset();
423 p = firstWhitespaceToRight(doc, p);
428 if (looksLike(doc, p, "try") //$NON-NLS-1$
429 || looksLike(doc, p, "do") //$NON-NLS-1$
430 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
431 || looksLike(doc, p, "static") //$NON-NLS-1$
432 || looksLike(doc, p, "finally") //$NON-NLS-1$
433 || looksLike(doc, p, "else")) //$NON-NLS-1$
434 return p + 1 - line.getOffset();
440 * Computes an insert position for an opening brace if <code>offset</code>
441 * maps to a position in <code>document</code> with a expression in
442 * parenthesis that will take a block after the closing parenthesis.
445 * the document being modified
447 * the current line under investigation
449 * the offset of the caret position, relative to the line start.
450 * @param partitioning
451 * the document partitioning
452 * @return an insert position relative to the line start if
453 * <code>line</code> contains a parenthesized expression that can
454 * be followed by a block, -1 otherwise
456 private static int computeAfterParenthesis(IDocument document,
457 ITextSelection line, int offset, String partitioning) {
458 // find the opening parenthesis for every closing parenthesis on the
459 // current line after offset
460 // return the position behind the closing parenthesis if it looks like a
461 // method declaration
462 // or an expression for an if, while, for, catch statement
463 int pos = offset + line.getOffset();
464 int length = line.getOffset() + line.getLength();
465 int scanTo = scanForward(document, pos, partitioning, length, '}');
469 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
472 int startScan = closingParen + 1;
473 closingParen = scanForward(document, startScan, partitioning,
475 if (closingParen == -1)
478 int openingParen = findOpeningParenMatch(document, closingParen,
481 // no way an expression at the beginning of the document can mean
483 if (openingParen < 1)
486 // only select insert positions for parenthesis currently embracing
488 if (openingParen > pos)
491 if (looksLikeAnonymousClassDef(document, openingParen - 1,
493 return closingParen + 1 - line.getOffset();
495 if (looksLikeIfWhileForCatch(document, openingParen - 1,
497 return closingParen + 1 - line.getOffset();
499 if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
500 return closingParen + 1 - line.getOffset();
508 * Finds a closing parenthesis to the left of <code>position</code> in
509 * document, where that parenthesis is only separated by whitespace from
510 * <code>position</code>. If no such parenthesis can be found,
511 * <code>position</code> is returned.
514 * the document being modified
516 * the first character position in <code>document</code> to be
518 * @param partitioning
519 * the document partitioning
520 * @return the position of a closing parenthesis left to
521 * <code>position</code> separated only by whitespace, or
522 * <code>position</code> if no parenthesis can be found
524 private static int findClosingParenToLeft(IDocument document, int position,
525 String partitioning) {
526 final char CLOSING_PAREN = ')';
531 int nonWS = firstNonWhitespaceBackward(document, position - 1,
533 if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
535 } catch (BadLocationException e1) {
541 * Finds the first whitespace character position to the right of (and
542 * including) <code>position</code>.
545 * the document being modified
547 * the first character position in <code>document</code> to be
549 * @return the position of a whitespace character greater or equal than
550 * <code>position</code> separated only by whitespace, or -1 if
553 private static int firstWhitespaceToRight(IDocument document, int position) {
554 int length = document.getLength();
555 Assert.isTrue(position >= 0);
556 Assert.isTrue(position <= length);
559 while (position < length) {
560 char ch = document.getChar(position);
561 if (Character.isWhitespace(ch))
566 } catch (BadLocationException e) {
572 * Finds the highest position in <code>document</code> such that the
573 * position is <= <code>position</code> and > <code>bound</code>
574 * and <code>Character.isWhitespace(document.getChar(pos))</code>
575 * evaluates to <code>false</code> and the position is in the default
579 * the document being modified
581 * the first character position in <code>document</code> to be
583 * @param partitioning
584 * the document partitioning
586 * the first position in <code>document</code> to not consider
587 * any more, with <code>bound</code> > <code>position</code>
588 * @return the highest position of one element in <code>chars</code> in [<code>position</code>,
589 * <code>scanTo</code>) that resides in a Java partition, or
590 * <code>-1</code> if none can be found
592 private static int firstNonWhitespaceBackward(IDocument document,
593 int position, String partitioning, int bound) {
594 Assert.isTrue(position < document.getLength());
595 Assert.isTrue(bound >= -1);
598 while (position > bound) {
599 char ch = document.getChar(position);
600 if (!Character.isWhitespace(ch)
601 && isDefaultPartition(document, position, partitioning))
605 } catch (BadLocationException e) {
611 * Finds the smallest position in <code>document</code> such that the
612 * position is >= <code>position</code> and < <code>bound</code>
613 * and <code>Character.isWhitespace(document.getChar(pos))</code>
614 * evaluates to <code>false</code> and the position is in the default
618 * the document being modified
620 * the first character position in <code>document</code> to be
622 * @param partitioning
623 * the document partitioning
625 * the first position in <code>document</code> to not consider
626 * any more, with <code>bound</code> > <code>position</code>
627 * @return the smallest position of one element in <code>chars</code> in [<code>position</code>,
628 * <code>scanTo</code>) that resides in a Java partition, or
629 * <code>-1</code> if none can be found
631 private static int firstNonWhitespaceForward(IDocument document,
632 int position, String partitioning, int bound) {
633 Assert.isTrue(position >= 0);
634 Assert.isTrue(bound <= document.getLength());
637 while (position < bound) {
638 char ch = document.getChar(position);
639 if (!Character.isWhitespace(ch)
640 && isDefaultPartition(document, position, partitioning))
644 } catch (BadLocationException e) {
650 * Finds the highest position in <code>document</code> such that the
651 * position is <= <code>position</code> and > <code>bound</code>
652 * and <code>document.getChar(position) == ch</code> evaluates to
653 * <code>true</code> for at least one ch in <code>chars</code> and the
654 * position is in the default partition.
657 * the document being modified
659 * the first character position in <code>document</code> to be
661 * @param partitioning
662 * the document partitioning
664 * the first position in <code>document</code> to not consider
665 * any more, with <code>scanTo</code> >
666 * <code>position</code>
668 * an array of <code>char</code> to search for
669 * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
670 * <code>position</code>] that resides in a Java partition, or
671 * <code>-1</code> if none can be found
673 private static int scanBackward(IDocument document, int position,
674 String partitioning, int bound, char[] chars) {
675 Assert.isTrue(bound >= -1);
676 Assert.isTrue(position < document.getLength());
681 while (position > bound) {
683 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
684 && isDefaultPartition(document, position, partitioning))
689 } catch (BadLocationException e) {
695 // * Finds the highest position in <code>document</code> such that the
696 // position is <= <code>position</code>
697 // * and > <code>bound</code> and <code>document.getChar(position) ==
698 // ch</code> evaluates to <code>true</code>
699 // * and the position is in the default partition.
701 // * @param document the document being modified
702 // * @param position the first character position in <code>document</code>
704 // * @param bound the first position in <code>document</code> to not
705 // consider any more, with <code>scanTo</code> > <code>position</code>
706 // * @param chars an array of <code>char</code> to search for
707 // * @return the highest position of one element in <code>chars</code> in
708 // [<code>position</code>, <code>scanTo</code>) that resides in a Java
709 // partition, or <code>-1</code> if none can be found
711 // private static int scanBackward(IDocument document, int position, int
713 // return scanBackward(document, position, bound, new char[] {ch});
717 * Finds the lowest position in <code>document</code> such that the
718 * position is >= <code>position</code> and < <code>bound</code>
719 * and <code>document.getChar(position) == ch</code> evaluates to
720 * <code>true</code> for at least one ch in <code>chars</code> and the
721 * position is in the default partition.
724 * the document being modified
726 * the first character position in <code>document</code> to be
728 * @param partitioning
729 * the document partitioning
731 * the first position in <code>document</code> to not consider
732 * any more, with <code>scanTo</code> >
733 * <code>position</code>
735 * an array of <code>char</code> to search for
736 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
737 * <code>bound</code>) that resides in a Java partition, or
738 * <code>-1</code> if none can be found
740 private static int scanForward(IDocument document, int position,
741 String partitioning, int bound, char[] chars) {
742 Assert.isTrue(position >= 0);
743 Assert.isTrue(bound <= document.getLength());
748 while (position < bound) {
750 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
751 && isDefaultPartition(document, position, partitioning))
756 } catch (BadLocationException e) {
762 * Finds the lowest position in <code>document</code> such that the
763 * position is >= <code>position</code> and < <code>bound</code>
764 * and <code>document.getChar(position) == ch</code> evaluates to
765 * <code>true</code> and the position is in the default partition.
768 * the document being modified
770 * the first character position in <code>document</code> to be
772 * @param partitioning
773 * the document partitioning
775 * the first position in <code>document</code> to not consider
776 * any more, with <code>scanTo</code> >
777 * <code>position</code>
779 * an array of <code>char</code> to search for
780 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
781 * <code>bound</code>) that resides in a Java partition, or
782 * <code>-1</code> if none can be found
784 private static int scanForward(IDocument document, int position,
785 String partitioning, int bound, char ch) {
786 return scanForward(document, position, partitioning, bound,
791 * Checks whether the content of <code>document</code> in the range (<code>offset</code>,
792 * <code>length</code>) contains the <code>new</code> keyword.
795 * the document being modified
797 * the first character position in <code>document</code> to be
800 * the length of the character range to be considered
801 * @param partitioning
802 * the document partitioning
803 * @return <code>true</code> if the specified character range contains a
804 * <code>new</code> keyword, <code>false</code> otherwise.
806 private static boolean isNewMatch(IDocument document, int offset,
807 int length, String partitioning) {
808 Assert.isTrue(length >= 0);
809 Assert.isTrue(offset >= 0);
810 Assert.isTrue(offset + length < document.getLength() + 1);
813 String text = document.get(offset, length);
814 int pos = text.indexOf("new"); //$NON-NLS-1$
817 && !isDefaultPartition(document, pos + offset, partitioning))
818 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
823 if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
827 && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
832 } catch (BadLocationException e) {
838 * Checks whether the content of <code>document</code> at
839 * <code>position</code> looks like an anonymous class definition.
840 * <code>position</code> must be to the left of the opening parenthesis of
841 * the definition's parameter list.
844 * the document being modified
846 * the first character position in <code>document</code> to be
848 * @param partitioning
849 * the document partitioning
850 * @return <code>true</code> if the content of <code>document</code>
851 * looks like an anonymous class definition, <code>false</code>
854 private static boolean looksLikeAnonymousClassDef(IDocument document,
855 int position, String partitioning) {
856 int previousCommaOrParen = scanBackward(document, position - 1,
857 partitioning, -1, new char[] { ',', '(' });
858 if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2
866 if (isNewMatch(document, previousCommaOrParen + 1, position
867 - previousCommaOrParen - 2, partitioning))
874 * Checks whether <code>position</code> resides in a default (Java)
875 * partition of <code>document</code>.
878 * the document being modified
880 * the position to be checked
881 * @param partitioning
882 * the document partitioning
883 * @return <code>true</code> if <code>position</code> is in the default
884 * partition of <code>document</code>, <code>false</code>
887 private static boolean isDefaultPartition(IDocument document, int position,
888 String partitioning) {
889 Assert.isTrue(position >= 0);
890 Assert.isTrue(position <= document.getLength());
893 // don't use getPartition2 since we're interested in the scanned
894 // character's partition
895 ITypedRegion region = TextUtilities.getPartition(document,
896 partitioning, position, false);
897 return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
899 } catch (BadLocationException e) {
906 * Finds the position of the parenthesis matching the closing parenthesis at
907 * <code>position</code>.
910 * the document being modified
912 * the position in <code>document</code> of a closing
914 * @param partitioning
915 * the document partitioning
916 * @return the position in <code>document</code> of the matching
917 * parenthesis, or -1 if none can be found
919 private static int findOpeningParenMatch(IDocument document, int position,
920 String partitioning) {
921 final char CLOSING_PAREN = ')';
922 final char OPENING_PAREN = '(';
924 Assert.isTrue(position < document.getLength());
925 Assert.isTrue(position >= 0);
926 Assert.isTrue(isDefaultPartition(document, position, partitioning));
930 Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
934 position = scanBackward(document, position - 1, partitioning,
935 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
939 if (document.getChar(position) == CLOSING_PAREN)
948 } catch (BadLocationException e) {
954 * Checks whether, to the left of <code>position</code> and separated only
955 * by whitespace, <code>document</code> contains a keyword taking a
956 * parameter list and a block after it. These are: <code>if</code>,
957 * <code>while</code>, <code>catch</code>, <code>for</code>,
958 * <code>synchronized</code>, <code>switch</code>.
961 * the document being modified
963 * the first character position in <code>document</code> to be
965 * @param partitioning
966 * the document partitioning
967 * @return <code>true</code> if <code>document</code> contains any of
968 * the above keywords to the left of <code>position</code>,
969 * <code>false</code> otherwise
971 private static boolean looksLikeIfWhileForCatch(IDocument document,
972 int position, String partitioning) {
973 position = firstNonWhitespaceBackward(document, position, partitioning,
978 return looksLike(document, position, "if") //$NON-NLS-1$
979 || looksLike(document, position, "while") //$NON-NLS-1$
980 || looksLike(document, position, "catch") //$NON-NLS-1$
981 || looksLike(document, position, "synchronized") //$NON-NLS-1$
982 || looksLike(document, position, "switch") //$NON-NLS-1$
983 || looksLike(document, position, "for"); //$NON-NLS-1$
987 * Checks whether code>document</code> contains the <code>String</code> <code>like</code>
988 * such that its last character is at <code>position</code>. If <code>like</code>
989 * starts with a identifier part (as determined by
990 * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that
991 * <code>like</code> is preceded by some non-identifier character or
992 * stands at the document start.
995 * the document being modified
997 * the first character position in <code>document</code> to be
1000 * the <code>String</code> to look for.
1001 * @return <code>true</code> if <code>document</code> contains <code>like</code>
1002 * such that it ends at <code>position</code>, <code>false</code>
1005 private static boolean looksLike(IDocument document, int position,
1007 int length = like.length();
1008 if (position < length - 1)
1012 if (!like.equals(document.get(position - length + 1, length)))
1015 if (position >= length
1016 && Scanner.isPHPIdentifierPart(like.charAt(0))
1017 && Scanner.isPHPIdentifierPart(document.getChar(position
1021 } catch (BadLocationException e) {
1029 * Checks whether the content of <code>document</code> at
1030 * <code>position</code> looks like a method declaration header (i.e. only
1031 * the return type and method name). <code>position</code> must be just
1032 * left of the opening parenthesis of the parameter list.
1035 * the document being modified
1037 * the first character position in <code>document</code> to be
1039 * @param partitioning
1040 * the document partitioning
1041 * @return <code>true</code> if the content of <code>document</code>
1042 * looks like a method definition, <code>false</code> otherwise
1044 private static boolean looksLikeMethodDecl(IDocument document,
1045 int position, String partitioning) {
1048 position = eatIdentToLeft(document, position, partitioning);
1052 position = eatBrackets(document, position - 1, partitioning);
1056 position = eatIdentToLeft(document, position - 1, partitioning);
1058 return position != -1;
1062 * From <code>position</code> to the left, eats any whitespace and then a
1063 * pair of brackets as used to declare an array return type like
1067 * </pre>. The return value is either the position of the opening bracket
1068 * or <code>position</code> if no pair of brackets can be parsed.
1071 * the document being modified
1073 * the first character position in <code>document</code> to be
1075 * @param partitioning
1076 * the document partitioning
1077 * @return the smallest character position of bracket pair or
1078 * <code>position</code>
1080 private static int eatBrackets(IDocument document, int position,
1081 String partitioning) {
1082 // accept array return type
1083 int pos = firstNonWhitespaceBackward(document, position, partitioning,
1086 if (pos > 1 && document.getChar(pos) == ']') {
1087 pos = firstNonWhitespaceBackward(document, pos - 1,
1089 if (pos > 0 && document.getChar(pos) == '[')
1092 } catch (BadLocationException e) {
1099 * From <code>position</code> to the left, eats any whitespace and the
1100 * first identifier, returning the position of the first identifier
1101 * character (in normal read order).
1103 * When called on a document with content <code>" some string "</code> and
1104 * positionition 13, the return value will be 6 (the first letter in
1105 * <code>string</code>).
1109 * the document being modified
1111 * the first character position in <code>document</code> to be
1113 * @param partitioning
1114 * the document partitioning
1115 * @return the smallest character position of an identifier or -1 if none
1116 * can be found; always <= <code>position</code>
1118 private static int eatIdentToLeft(IDocument document, int position,
1119 String partitioning) {
1122 Assert.isTrue(position < document.getLength());
1124 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
1131 char ch = document.getChar(p);
1132 if (Scanner.isPHPIdentifierPart(ch)) {
1137 // length must be > 0
1138 if (Character.isWhitespace(ch) && p != position)
1145 // start of document reached
1148 } catch (BadLocationException e) {
1154 * Returns a position in the first java partition after the last non-empty
1155 * and non-comment partition. There is no non-whitespace from the returned
1156 * position to the end of the partition it is contained in.
1159 * the document being modified
1161 * the line under investigation
1163 * the caret offset into <code>line</code>
1164 * @param partitioning
1165 * the document partitioning
1166 * @return the position of the next Java partition, or the end of
1169 private static int nextPartitionOrLineEnd(IDocument document,
1170 ITextSelection line, int offset, String partitioning) {
1171 // run relative to document
1172 final int docOffset = offset + line.getOffset();
1173 final int eol = line.getOffset() + line.getLength();
1174 int nextPartitionPos = eol; // init with line end
1175 int validPosition = docOffset;
1178 ITypedRegion partition = TextUtilities.getPartition(document,
1179 partitioning, nextPartitionPos, true);
1180 validPosition = getValidPositionForPartition(document, partition,
1182 while (validPosition == -1) {
1183 nextPartitionPos = partition.getOffset() - 1;
1184 if (nextPartitionPos < docOffset) {
1185 validPosition = docOffset;
1188 partition = TextUtilities.getPartition(document, partitioning,
1189 nextPartitionPos, false);
1190 validPosition = getValidPositionForPartition(document,
1193 } catch (BadLocationException e) {
1196 validPosition = Math.max(validPosition, docOffset);
1197 // make relative to line
1198 validPosition -= line.getOffset();
1199 return validPosition;
1203 * Returns a valid insert location (except for whitespace) in
1204 * <code>partition</code> or -1 if there is no valid insert location. An
1205 * valid insert location is right after any java string or character
1206 * partition, or at the end of a java default partition, but never behind
1207 * <code>maxOffset</code>. Comment partitions or empty java partitions do
1208 * never yield valid insert positions.
1211 * the document being modified
1213 * the current partition
1215 * the maximum offset to consider
1216 * @return a valid insert location in <code>partition</code>, or -1 if
1217 * there is no valid insert location
1219 private static int getValidPositionForPartition(IDocument doc,
1220 ITypedRegion partition, int maxOffset) {
1221 final int INVALID = -1;
1223 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
1225 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
1227 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
1230 int endOffset = Math.min(maxOffset, partition.getOffset()
1231 + partition.getLength());
1233 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
1234 // return endOffset;
1235 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
1237 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
1239 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
1241 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1243 if (doc.get(partition.getOffset(),
1244 endOffset - partition.getOffset()).trim().length() == 0)
1248 } catch (BadLocationException e) {
1252 // default: we don't know anything about the partition - assume valid
1257 * Determines whether the current line contains a for statement. Algorithm:
1258 * any "for" word in the line is a positive, "for" contained in a string
1259 * literal will produce a false positive.
1262 * the line where the change is being made
1264 * the position of the caret
1265 * @return <code>true</code> if <code>line</code> contains
1266 * <code>for</code>, <code>false</code> otherwise
1268 private static boolean isForStatement(String line, int offset) {
1269 /* searching for (^|\s)for(\s|$) */
1270 int forPos = line.indexOf("for"); //$NON-NLS-1$
1272 if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
1273 .charAt(forPos - 1)))
1274 && (line.length() == forPos + 3 || !Scanner
1275 .isPHPIdentifierPart(line.charAt(forPos + 3))))
1282 * Returns the position in <code>text</code> after which there comes only
1283 * whitespace, up to <code>offset</code>.
1286 * the text being searched
1288 * the maximum offset to search for
1289 * @return the smallest value <code>v</code> such that
1290 * <code>text.substring(v, offset).trim() == 0</code>
1292 private static int startOfWhitespaceBeforeOffset(String text, int offset) {
1293 int i = Math.min(offset, text.length());
1294 for (; i >= 1; i--) {
1295 if (!Character.isWhitespace(text.charAt(i - 1)))