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;
16 import net.sourceforge.phpdt.internal.core.Assert;
17 import net.sourceforge.phpdt.internal.ui.text.SmartBackspaceManager.UndoSpec;
18 import net.sourceforge.phpdt.ui.PreferenceConstants;
19 //import net.sourceforge.phpeclipse.PHPeclipsePlugin;
20 import net.sourceforge.phpeclipse.phpeditor.PHPUnitEditor;
21 import net.sourceforge.phpeclipse.ui.WebUI;
23 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;
34 import org.eclipse.text.edits.DeleteEdit;
35 import org.eclipse.text.edits.MalformedTreeException;
36 import org.eclipse.text.edits.ReplaceEdit;
37 import org.eclipse.text.edits.TextEdit;
38 import org.eclipse.ui.IEditorPart;
39 import org.eclipse.ui.IWorkbenchPage;
40 import org.eclipse.ui.texteditor.ITextEditorExtension2;
41 import org.eclipse.ui.texteditor.ITextEditorExtension3;
44 * Modifies <code>DocumentCommand</code>s inserting semicolons and opening
45 * braces to place them smartly, i.e. moving them to the end of a line if that
46 * is what the user expects.
49 * In practice, semicolons and braces (and the caret) are moved to the end of
50 * the line if they are typed anywhere except for semicolons in a
51 * <code>for</code> statements definition. If the line contains a semicolon or
52 * brace after the current caret position, the cursor is moved after it.
55 * @see org.eclipse.jface.text.DocumentCommand
58 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
60 /** String representation of a semicolon. */
61 private static final String SEMICOLON = ";"; //$NON-NLS-1$
63 /** Char representation of a semicolon. */
64 private static final char SEMICHAR = ';';
66 /** String represenattion of a opening brace. */
67 private static final String BRACE = "{"; //$NON-NLS-1$
69 /** Char representation of a opening brace */
70 private static final char BRACECHAR = '{';
72 private char fCharacter;
74 private String fPartitioning;
77 * Creates a new SmartSemicolonAutoEditStrategy.
80 * the document partitioning
82 public SmartSemicolonAutoEditStrategy(String partitioning) {
83 fPartitioning = partitioning;
87 * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
88 * org.eclipse.jface.text.DocumentCommand)
90 public void customizeDocumentCommand(IDocument document,
91 DocumentCommand command) {
93 // also customize if <code>doit</code> is false (so it works in code
94 // completion situations)
98 if (command.text == null)
101 if (command.text.equals(SEMICOLON))
102 fCharacter = SEMICHAR;
103 else if (command.text.equals(BRACE))
104 fCharacter = BRACECHAR;
108 IPreferenceStore store = WebUI.getDefault()
109 .getPreferenceStore();
110 if (fCharacter == SEMICHAR
112 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
114 if (fCharacter == BRACECHAR
116 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
119 IWorkbenchPage page = WebUI.getActivePage();
122 IEditorPart part = page.getActiveEditor();
123 if (!(part instanceof PHPUnitEditor))
125 PHPUnitEditor editor = (PHPUnitEditor) part;
126 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
127 || !editor.isEditable())
129 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
130 .getAdapter(ITextEditorExtension2.class);
131 if (extension != null && !extension.validateEditorInputState())
133 if (isMultilineSelection(document, command))
136 // 1: find concerned line / position in java code, location in statement
137 int pos = command.offset;
140 IRegion l = document.getLineInformationOfOffset(pos);
141 line = new TextSelection(document, l.getOffset(), l.getLength());
142 } catch (BadLocationException e) {
146 // 2: choose action based on findings (is for-Statement?)
147 // for now: compute the best position to insert the new character
148 int positionInLine = computeCharacterPosition(document, line, pos
149 - line.getOffset(), fCharacter, fPartitioning);
150 int position = positionInLine + line.getOffset();
152 // never position before the current position!
156 // never double already existing content
157 if (alreadyPresent(document, fCharacter, position))
160 // don't do special processing if what we do is actually the normal
162 String insertion = adjustSpacing(document, position, fCharacter);
163 if (command.offset == position && insertion.equals(command.text))
168 final SmartBackspaceManager manager = (SmartBackspaceManager) editor
169 .getAdapter(SmartBackspaceManager.class);
171 && WebUI.getDefault().getPreferenceStore()
173 PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
174 TextEdit e1 = new ReplaceEdit(command.offset, command.text
175 .length(), document.get(command.offset, command.length));
176 UndoSpec s1 = new UndoSpec(command.offset
177 + command.text.length(), new Region(command.offset, 0),
178 new TextEdit[] { e1 }, 0, null);
180 DeleteEdit smart = new DeleteEdit(position, insertion.length());
181 ReplaceEdit raw = new ReplaceEdit(command.offset,
182 command.length, command.text);
183 UndoSpec s2 = new UndoSpec(position + insertion.length(),
184 new Region(command.offset + command.text.length(), 0),
185 new TextEdit[] { smart, raw }, 2, s1);
186 manager.register(s2);
190 command.offset = position;
192 command.caretOffset = position;
193 command.text = insertion;
195 command.owner = null;
196 } catch (MalformedTreeException e) {
198 } catch (BadLocationException e) {
205 * Returns <code>true</code> if the document command is applied on a multi
206 * line selection, <code>false</code> otherwise.
212 * @return <code>true</code> if <code>command</code> is a multiline
215 private boolean isMultilineSelection(IDocument document,
216 DocumentCommand command) {
218 return document.getNumberOfLines(command.offset, command.length) > 1;
219 } catch (BadLocationException e) {
226 * Adds a space before a brace if it is inserted after a parenthesis, equal
227 * sign, or one of the keywords <code>try, else, do</code>.
230 * the document we are working on
232 * the insert position of <code>character</code>
234 * the character to be inserted
235 * @return a <code>String</code> consisting of <code>character</code>
236 * plus any additional spacing
238 private String adjustSpacing(IDocument doc, int position, char character) {
239 if (character == BRACECHAR) {
240 if (position > 0 && position <= doc.getLength()) {
241 int pos = position - 1;
242 if (looksLike(doc, pos, ")") //$NON-NLS-1$
243 || looksLike(doc, pos, "=") //$NON-NLS-1$
244 || looksLike(doc, pos, "]") //$NON-NLS-1$
245 || looksLike(doc, pos, "try") //$NON-NLS-1$
246 || looksLike(doc, pos, "else") //$NON-NLS-1$
247 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
248 || looksLike(doc, pos, "static") //$NON-NLS-1$
249 || looksLike(doc, pos, "finally") //$NON-NLS-1$
250 || looksLike(doc, pos, "do")) //$NON-NLS-1$
251 return new String(new char[] { ' ', character });
255 return new String(new char[] { character });
259 * Checks whether a character to be inserted is already present at the
260 * insert location (perhaps separated by some whitespace from
261 * <code>position</code>.
264 * the document we are working on
266 * the insert position of <code>ch</code>
268 * the character to be inserted
269 * @return <code>true</code> if <code>ch</code> is already present at
270 * <code>location</code>, <code>false</code> otherwise
272 private boolean alreadyPresent(IDocument document, char ch, int position) {
273 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
274 document.getLength());
276 if (pos != -1 && document.getChar(pos) == ch)
278 } catch (BadLocationException e) {
285 * Computes the next insert position of the given character in the current
289 * the document we are working on
291 * the line where the change is being made
293 * the position of the caret in the line when
294 * <code>character</code> was typed
296 * the character to look for
297 * @param partitioning
298 * the document partitioning
299 * @return the position where <code>character</code> should be inserted /
302 protected static int computeCharacterPosition(IDocument document,
303 ITextSelection line, int offset, char character, String partitioning) {
304 String text = line.getText();
309 if (character == BRACECHAR) {
311 insertPos = computeArrayInitializationPos(document, line, offset,
314 if (insertPos == -1) {
315 insertPos = computeAfterTryDoElse(document, line, offset);
318 if (insertPos == -1) {
319 insertPos = computeAfterParenthesis(document, line, offset,
323 } else if (character == SEMICHAR) {
325 if (isForStatement(text, offset)) {
326 insertPos = -1; // don't do anything in for statements, as semis
327 // are vital part of these
329 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
330 offset, partitioning);
331 insertPos = startOfWhitespaceBeforeOffset(text,
333 // if there is a semi present, return its location as
334 // alreadyPresent() will take it out this way.
335 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
336 insertPos = insertPos - 1;
340 Assert.isTrue(false);
348 * Computes an insert position for an opening brace if <code>offset</code>
349 * maps to a position in <code>document</code> that looks like being the
350 * RHS of an assignment or like an array definition.
353 * the document being modified
355 * the current line under investigation
357 * the offset of the caret position, relative to the line start.
358 * @param partitioning
359 * the document partitioning
360 * @return an insert position relative to the line start if
361 * <code>line</code> looks like being an array initialization at
362 * <code>offset</code>, -1 otherwise
364 private static int computeArrayInitializationPos(IDocument document,
365 ITextSelection line, int offset, String partitioning) {
366 // search backward while WS, find = (not != <= >= ==) in default
368 int pos = offset + line.getOffset();
373 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
380 char ch = document.getChar(p);
381 if (ch != '=' && ch != ']')
387 p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
391 ch = document.getChar(p);
392 if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
395 } catch (BadLocationException e) {
401 * Computes an insert position for an opening brace if <code>offset</code>
402 * maps to a position in <code>document</code> involving a keyword taking
403 * a block after it. These are: <code>try</code>, <code>do</code>,
404 * <code>synchronized</code>, <code>static</code>,
405 * <code>finally</code>, or <code>else</code>.
408 * the document being modified
410 * the current line under investigation
412 * the offset of the caret position, relative to the line start.
413 * @return an insert position relative to the line start if
414 * <code>line</code> contains one of the above keywords at or
415 * before <code>offset</code>, -1 otherwise
417 private static int computeAfterTryDoElse(IDocument doc,
418 ITextSelection line, int offset) {
419 // search backward while WS, find 'try', 'do', 'else' in default
421 int p = offset + line.getOffset();
422 p = firstWhitespaceToRight(doc, p);
427 if (looksLike(doc, p, "try") //$NON-NLS-1$
428 || looksLike(doc, p, "do") //$NON-NLS-1$
429 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
430 || looksLike(doc, p, "static") //$NON-NLS-1$
431 || looksLike(doc, p, "finally") //$NON-NLS-1$
432 || looksLike(doc, p, "else")) //$NON-NLS-1$
433 return p + 1 - line.getOffset();
439 * Computes an insert position for an opening brace if <code>offset</code>
440 * maps to a position in <code>document</code> with a expression in
441 * parenthesis that will take a block after the closing parenthesis.
444 * the document being modified
446 * the current line under investigation
448 * the offset of the caret position, relative to the line start.
449 * @param partitioning
450 * the document partitioning
451 * @return an insert position relative to the line start if
452 * <code>line</code> contains a parenthesized expression that can
453 * be followed by a block, -1 otherwise
455 private static int computeAfterParenthesis(IDocument document,
456 ITextSelection line, int offset, String partitioning) {
457 // find the opening parenthesis for every closing parenthesis on the
458 // current line after offset
459 // return the position behind the closing parenthesis if it looks like a
460 // method declaration
461 // or an expression for an if, while, for, catch statement
462 int pos = offset + line.getOffset();
463 int length = line.getOffset() + line.getLength();
464 int scanTo = scanForward(document, pos, partitioning, length, '}');
468 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
471 int startScan = closingParen + 1;
472 closingParen = scanForward(document, startScan, partitioning,
474 if (closingParen == -1)
477 int openingParen = findOpeningParenMatch(document, closingParen,
480 // no way an expression at the beginning of the document can mean
482 if (openingParen < 1)
485 // only select insert positions for parenthesis currently embracing
487 if (openingParen > pos)
490 if (looksLikeAnonymousClassDef(document, openingParen - 1,
492 return closingParen + 1 - line.getOffset();
494 if (looksLikeIfWhileForCatch(document, openingParen - 1,
496 return closingParen + 1 - line.getOffset();
498 if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
499 return closingParen + 1 - line.getOffset();
507 * Finds a closing parenthesis to the left of <code>position</code> in
508 * document, where that parenthesis is only separated by whitespace from
509 * <code>position</code>. If no such parenthesis can be found,
510 * <code>position</code> is returned.
513 * the document being modified
515 * the first character position in <code>document</code> to be
517 * @param partitioning
518 * the document partitioning
519 * @return the position of a closing parenthesis left to
520 * <code>position</code> separated only by whitespace, or
521 * <code>position</code> if no parenthesis can be found
523 private static int findClosingParenToLeft(IDocument document, int position,
524 String partitioning) {
525 final char CLOSING_PAREN = ')';
530 int nonWS = firstNonWhitespaceBackward(document, position - 1,
532 if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
534 } catch (BadLocationException e1) {
540 * Finds the first whitespace character position to the right of (and
541 * including) <code>position</code>.
544 * the document being modified
546 * the first character position in <code>document</code> to be
548 * @return the position of a whitespace character greater or equal than
549 * <code>position</code> separated only by whitespace, or -1 if
552 private static int firstWhitespaceToRight(IDocument document, int position) {
553 int length = document.getLength();
554 Assert.isTrue(position >= 0);
555 Assert.isTrue(position <= length);
558 while (position < length) {
559 char ch = document.getChar(position);
560 if (Character.isWhitespace(ch))
565 } catch (BadLocationException e) {
571 * Finds the highest position in <code>document</code> such that the
572 * position is <= <code>position</code> and > <code>bound</code>
573 * and <code>Character.isWhitespace(document.getChar(pos))</code>
574 * evaluates to <code>false</code> and the position is in the default
578 * the document being modified
580 * the first character position in <code>document</code> to be
582 * @param partitioning
583 * the document partitioning
585 * the first position in <code>document</code> to not consider
586 * any more, with <code>bound</code> > <code>position</code>
587 * @return the highest position of one element in <code>chars</code> in [<code>position</code>,
588 * <code>scanTo</code>) that resides in a Java partition, or
589 * <code>-1</code> if none can be found
591 private static int firstNonWhitespaceBackward(IDocument document,
592 int position, String partitioning, int bound) {
593 Assert.isTrue(position < document.getLength());
594 Assert.isTrue(bound >= -1);
597 while (position > bound) {
598 char ch = document.getChar(position);
599 if (!Character.isWhitespace(ch)
600 && isDefaultPartition(document, position, partitioning))
604 } catch (BadLocationException e) {
610 * Finds the smallest position in <code>document</code> such that the
611 * position is >= <code>position</code> and < <code>bound</code>
612 * and <code>Character.isWhitespace(document.getChar(pos))</code>
613 * evaluates to <code>false</code> and the position is in the default
617 * the document being modified
619 * the first character position in <code>document</code> to be
621 * @param partitioning
622 * the document partitioning
624 * the first position in <code>document</code> to not consider
625 * any more, with <code>bound</code> > <code>position</code>
626 * @return the smallest position of one element in <code>chars</code> in [<code>position</code>,
627 * <code>scanTo</code>) that resides in a Java partition, or
628 * <code>-1</code> if none can be found
630 private static int firstNonWhitespaceForward(IDocument document,
631 int position, String partitioning, int bound) {
632 Assert.isTrue(position >= 0);
633 Assert.isTrue(bound <= document.getLength());
636 while (position < bound) {
637 char ch = document.getChar(position);
638 if (!Character.isWhitespace(ch)
639 && isDefaultPartition(document, position, partitioning))
643 } catch (BadLocationException e) {
649 * Finds the highest position in <code>document</code> such that the
650 * position is <= <code>position</code> and > <code>bound</code>
651 * and <code>document.getChar(position) == ch</code> evaluates to
652 * <code>true</code> for at least one ch in <code>chars</code> and the
653 * position is in the default partition.
656 * the document being modified
658 * the first character position in <code>document</code> to be
660 * @param partitioning
661 * the document partitioning
663 * the first position in <code>document</code> to not consider
664 * any more, with <code>scanTo</code> >
665 * <code>position</code>
667 * an array of <code>char</code> to search for
668 * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
669 * <code>position</code>] that resides in a Java partition, or
670 * <code>-1</code> if none can be found
672 private static int scanBackward(IDocument document, int position,
673 String partitioning, int bound, char[] chars) {
674 Assert.isTrue(bound >= -1);
675 Assert.isTrue(position < document.getLength());
680 while (position > bound) {
682 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
683 && isDefaultPartition(document, position, partitioning))
688 } catch (BadLocationException e) {
694 // * Finds the highest position in <code>document</code> such that the
695 // position is <= <code>position</code>
696 // * and > <code>bound</code> and <code>document.getChar(position) ==
697 // ch</code> evaluates to <code>true</code>
698 // * and the position is in the default partition.
700 // * @param document the document being modified
701 // * @param position the first character position in <code>document</code>
703 // * @param bound the first position in <code>document</code> to not
704 // consider any more, with <code>scanTo</code> > <code>position</code>
705 // * @param chars an array of <code>char</code> to search for
706 // * @return the highest position of one element in <code>chars</code> in
707 // [<code>position</code>, <code>scanTo</code>) that resides in a Java
708 // partition, or <code>-1</code> if none can be found
710 // private static int scanBackward(IDocument document, int position, int
712 // return scanBackward(document, position, bound, new char[] {ch});
716 * Finds the lowest position in <code>document</code> such that the
717 * position is >= <code>position</code> and < <code>bound</code>
718 * and <code>document.getChar(position) == ch</code> evaluates to
719 * <code>true</code> for at least one ch in <code>chars</code> and the
720 * position is in the default partition.
723 * the document being modified
725 * the first character position in <code>document</code> to be
727 * @param partitioning
728 * the document partitioning
730 * the first position in <code>document</code> to not consider
731 * any more, with <code>scanTo</code> >
732 * <code>position</code>
734 * an array of <code>char</code> to search for
735 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
736 * <code>bound</code>) that resides in a Java partition, or
737 * <code>-1</code> if none can be found
739 private static int scanForward(IDocument document, int position,
740 String partitioning, int bound, char[] chars) {
741 Assert.isTrue(position >= 0);
742 Assert.isTrue(bound <= document.getLength());
747 while (position < bound) {
749 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
750 && isDefaultPartition(document, position, partitioning))
755 } catch (BadLocationException e) {
761 * Finds the lowest position in <code>document</code> such that the
762 * position is >= <code>position</code> and < <code>bound</code>
763 * and <code>document.getChar(position) == ch</code> evaluates to
764 * <code>true</code> and the position is in the default partition.
767 * the document being modified
769 * the first character position in <code>document</code> to be
771 * @param partitioning
772 * the document partitioning
774 * the first position in <code>document</code> to not consider
775 * any more, with <code>scanTo</code> >
776 * <code>position</code>
778 * an array of <code>char</code> to search for
779 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
780 * <code>bound</code>) that resides in a Java partition, or
781 * <code>-1</code> if none can be found
783 private static int scanForward(IDocument document, int position,
784 String partitioning, int bound, char ch) {
785 return scanForward(document, position, partitioning, bound,
790 * Checks whether the content of <code>document</code> in the range (<code>offset</code>,
791 * <code>length</code>) contains the <code>new</code> keyword.
794 * the document being modified
796 * the first character position in <code>document</code> to be
799 * the length of the character range to be considered
800 * @param partitioning
801 * the document partitioning
802 * @return <code>true</code> if the specified character range contains a
803 * <code>new</code> keyword, <code>false</code> otherwise.
805 private static boolean isNewMatch(IDocument document, int offset,
806 int length, String partitioning) {
807 Assert.isTrue(length >= 0);
808 Assert.isTrue(offset >= 0);
809 Assert.isTrue(offset + length < document.getLength() + 1);
812 String text = document.get(offset, length);
813 int pos = text.indexOf("new"); //$NON-NLS-1$
816 && !isDefaultPartition(document, pos + offset, partitioning))
817 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
822 if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
826 && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
831 } catch (BadLocationException e) {
837 * Checks whether the content of <code>document</code> at
838 * <code>position</code> looks like an anonymous class definition.
839 * <code>position</code> must be to the left of the opening parenthesis of
840 * the definition's parameter list.
843 * the document being modified
845 * the first character position in <code>document</code> to be
847 * @param partitioning
848 * the document partitioning
849 * @return <code>true</code> if the content of <code>document</code>
850 * looks like an anonymous class definition, <code>false</code>
853 private static boolean looksLikeAnonymousClassDef(IDocument document,
854 int position, String partitioning) {
855 int previousCommaOrParen = scanBackward(document, position - 1,
856 partitioning, -1, new char[] { ',', '(' });
857 if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2
865 if (isNewMatch(document, previousCommaOrParen + 1, position
866 - previousCommaOrParen - 2, partitioning))
873 * Checks whether <code>position</code> resides in a default (Java)
874 * partition of <code>document</code>.
877 * the document being modified
879 * the position to be checked
880 * @param partitioning
881 * the document partitioning
882 * @return <code>true</code> if <code>position</code> is in the default
883 * partition of <code>document</code>, <code>false</code>
886 private static boolean isDefaultPartition(IDocument document, int position,
887 String partitioning) {
888 Assert.isTrue(position >= 0);
889 Assert.isTrue(position <= document.getLength());
892 // don't use getPartition2 since we're interested in the scanned
893 // character's partition
894 ITypedRegion region = TextUtilities.getPartition(document,
895 partitioning, position, false);
896 return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
898 } catch (BadLocationException e) {
905 * Finds the position of the parenthesis matching the closing parenthesis at
906 * <code>position</code>.
909 * the document being modified
911 * the position in <code>document</code> of a closing
913 * @param partitioning
914 * the document partitioning
915 * @return the position in <code>document</code> of the matching
916 * parenthesis, or -1 if none can be found
918 private static int findOpeningParenMatch(IDocument document, int position,
919 String partitioning) {
920 final char CLOSING_PAREN = ')';
921 final char OPENING_PAREN = '(';
923 Assert.isTrue(position < document.getLength());
924 Assert.isTrue(position >= 0);
925 Assert.isTrue(isDefaultPartition(document, position, partitioning));
929 Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
933 position = scanBackward(document, position - 1, partitioning,
934 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
938 if (document.getChar(position) == CLOSING_PAREN)
947 } catch (BadLocationException e) {
953 * Checks whether, to the left of <code>position</code> and separated only
954 * by whitespace, <code>document</code> contains a keyword taking a
955 * parameter list and a block after it. These are: <code>if</code>,
956 * <code>while</code>, <code>catch</code>, <code>for</code>,
957 * <code>synchronized</code>, <code>switch</code>.
960 * the document being modified
962 * the first character position in <code>document</code> to be
964 * @param partitioning
965 * the document partitioning
966 * @return <code>true</code> if <code>document</code> contains any of
967 * the above keywords to the left of <code>position</code>,
968 * <code>false</code> otherwise
970 private static boolean looksLikeIfWhileForCatch(IDocument document,
971 int position, String partitioning) {
972 position = firstNonWhitespaceBackward(document, position, partitioning,
977 return looksLike(document, position, "if") //$NON-NLS-1$
978 || looksLike(document, position, "while") //$NON-NLS-1$
979 || looksLike(document, position, "catch") //$NON-NLS-1$
980 || looksLike(document, position, "synchronized") //$NON-NLS-1$
981 || looksLike(document, position, "switch") //$NON-NLS-1$
982 || looksLike(document, position, "for"); //$NON-NLS-1$
986 * Checks whether code>document</code> contains the <code>String</code> <code>like</code>
987 * such that its last character is at <code>position</code>. If <code>like</code>
988 * starts with a identifier part (as determined by
989 * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that
990 * <code>like</code> is preceded by some non-identifier character or
991 * stands at the document start.
994 * the document being modified
996 * the first character position in <code>document</code> to be
999 * the <code>String</code> to look for.
1000 * @return <code>true</code> if <code>document</code> contains <code>like</code>
1001 * such that it ends at <code>position</code>, <code>false</code>
1004 private static boolean looksLike(IDocument document, int position,
1006 int length = like.length();
1007 if (position < length - 1)
1011 if (!like.equals(document.get(position - length + 1, length)))
1014 if (position >= length
1015 && Scanner.isPHPIdentifierPart(like.charAt(0))
1016 && Scanner.isPHPIdentifierPart(document.getChar(position
1020 } catch (BadLocationException e) {
1028 * Checks whether the content of <code>document</code> at
1029 * <code>position</code> looks like a method declaration header (i.e. only
1030 * the return type and method name). <code>position</code> must be just
1031 * left of the opening parenthesis of the parameter list.
1034 * the document being modified
1036 * the first character position in <code>document</code> to be
1038 * @param partitioning
1039 * the document partitioning
1040 * @return <code>true</code> if the content of <code>document</code>
1041 * looks like a method definition, <code>false</code> otherwise
1043 private static boolean looksLikeMethodDecl(IDocument document,
1044 int position, String partitioning) {
1047 position = eatIdentToLeft(document, position, partitioning);
1051 position = eatBrackets(document, position - 1, partitioning);
1055 position = eatIdentToLeft(document, position - 1, partitioning);
1057 return position != -1;
1061 * From <code>position</code> to the left, eats any whitespace and then a
1062 * pair of brackets as used to declare an array return type like
1066 * </pre>. The return value is either the position of the opening bracket
1067 * or <code>position</code> if no pair of brackets can be parsed.
1070 * the document being modified
1072 * the first character position in <code>document</code> to be
1074 * @param partitioning
1075 * the document partitioning
1076 * @return the smallest character position of bracket pair or
1077 * <code>position</code>
1079 private static int eatBrackets(IDocument document, int position,
1080 String partitioning) {
1081 // accept array return type
1082 int pos = firstNonWhitespaceBackward(document, position, partitioning,
1085 if (pos > 1 && document.getChar(pos) == ']') {
1086 pos = firstNonWhitespaceBackward(document, pos - 1,
1088 if (pos > 0 && document.getChar(pos) == '[')
1091 } catch (BadLocationException e) {
1098 * From <code>position</code> to the left, eats any whitespace and the
1099 * first identifier, returning the position of the first identifier
1100 * character (in normal read order).
1102 * When called on a document with content <code>" some string "</code> and
1103 * positionition 13, the return value will be 6 (the first letter in
1104 * <code>string</code>).
1108 * the document being modified
1110 * the first character position in <code>document</code> to be
1112 * @param partitioning
1113 * the document partitioning
1114 * @return the smallest character position of an identifier or -1 if none
1115 * can be found; always <= <code>position</code>
1117 private static int eatIdentToLeft(IDocument document, int position,
1118 String partitioning) {
1121 Assert.isTrue(position < document.getLength());
1123 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
1130 char ch = document.getChar(p);
1131 if (Scanner.isPHPIdentifierPart(ch)) {
1136 // length must be > 0
1137 if (Character.isWhitespace(ch) && p != position)
1144 // start of document reached
1147 } catch (BadLocationException e) {
1153 * Returns a position in the first java partition after the last non-empty
1154 * and non-comment partition. There is no non-whitespace from the returned
1155 * position to the end of the partition it is contained in.
1158 * the document being modified
1160 * the line under investigation
1162 * the caret offset into <code>line</code>
1163 * @param partitioning
1164 * the document partitioning
1165 * @return the position of the next Java partition, or the end of
1168 private static int nextPartitionOrLineEnd(IDocument document,
1169 ITextSelection line, int offset, String partitioning) {
1170 // run relative to document
1171 final int docOffset = offset + line.getOffset();
1172 final int eol = line.getOffset() + line.getLength();
1173 int nextPartitionPos = eol; // init with line end
1174 int validPosition = docOffset;
1177 ITypedRegion partition = TextUtilities.getPartition(document,
1178 partitioning, nextPartitionPos, true);
1179 validPosition = getValidPositionForPartition(document, partition,
1181 while (validPosition == -1) {
1182 nextPartitionPos = partition.getOffset() - 1;
1183 if (nextPartitionPos < docOffset) {
1184 validPosition = docOffset;
1187 partition = TextUtilities.getPartition(document, partitioning,
1188 nextPartitionPos, false);
1189 validPosition = getValidPositionForPartition(document,
1192 } catch (BadLocationException e) {
1195 validPosition = Math.max(validPosition, docOffset);
1196 // make relative to line
1197 validPosition -= line.getOffset();
1198 return validPosition;
1202 * Returns a valid insert location (except for whitespace) in
1203 * <code>partition</code> or -1 if there is no valid insert location. An
1204 * valid insert location is right after any java string or character
1205 * partition, or at the end of a java default partition, but never behind
1206 * <code>maxOffset</code>. Comment partitions or empty java partitions do
1207 * never yield valid insert positions.
1210 * the document being modified
1212 * the current partition
1214 * the maximum offset to consider
1215 * @return a valid insert location in <code>partition</code>, or -1 if
1216 * there is no valid insert location
1218 private static int getValidPositionForPartition(IDocument doc,
1219 ITypedRegion partition, int maxOffset) {
1220 final int INVALID = -1;
1222 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
1224 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
1226 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
1229 int endOffset = Math.min(maxOffset, partition.getOffset()
1230 + partition.getLength());
1232 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
1233 // return endOffset;
1234 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
1236 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
1238 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
1240 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1242 if (doc.get(partition.getOffset(),
1243 endOffset - partition.getOffset()).trim().length() == 0)
1247 } catch (BadLocationException e) {
1251 // default: we don't know anything about the partition - assume valid
1256 * Determines whether the current line contains a for statement. Algorithm:
1257 * any "for" word in the line is a positive, "for" contained in a string
1258 * literal will produce a false positive.
1261 * the line where the change is being made
1263 * the position of the caret
1264 * @return <code>true</code> if <code>line</code> contains
1265 * <code>for</code>, <code>false</code> otherwise
1267 private static boolean isForStatement(String line, int offset) {
1268 /* searching for (^|\s)for(\s|$) */
1269 int forPos = line.indexOf("for"); //$NON-NLS-1$
1271 if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
1272 .charAt(forPos - 1)))
1273 && (line.length() == forPos + 3 || !Scanner
1274 .isPHPIdentifierPart(line.charAt(forPos + 3))))
1281 * Returns the position in <code>text</code> after which there comes only
1282 * whitespace, up to <code>offset</code>.
1285 * the text being searched
1287 * the maximum offset to search for
1288 * @return the smallest value <code>v</code> such that
1289 * <code>text.substring(v, offset).trim() == 0</code>
1291 private static int startOfWhitespaceBeforeOffset(String text, int offset) {
1292 int i = Math.min(offset, text.length());
1293 for (; i >= 1; i--) {
1294 if (!Character.isWhitespace(text.charAt(i - 1)))