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;
22 import org.eclipse.jface.preference.IPreferenceStore;
23 import org.eclipse.jface.text.BadLocationException;
24 import org.eclipse.jface.text.DocumentCommand;
25 import org.eclipse.jface.text.IAutoEditStrategy;
26 import org.eclipse.jface.text.IDocument;
27 import org.eclipse.jface.text.IRegion;
28 import org.eclipse.jface.text.ITextSelection;
29 import org.eclipse.jface.text.ITypedRegion;
30 import org.eclipse.jface.text.Region;
31 import org.eclipse.jface.text.TextSelection;
32 import org.eclipse.jface.text.TextUtilities;
33 import org.eclipse.text.edits.DeleteEdit;
34 import org.eclipse.text.edits.MalformedTreeException;
35 import org.eclipse.text.edits.ReplaceEdit;
36 import org.eclipse.text.edits.TextEdit;
37 import org.eclipse.ui.IEditorPart;
38 import org.eclipse.ui.IWorkbenchPage;
39 import org.eclipse.ui.texteditor.ITextEditorExtension2;
40 import org.eclipse.ui.texteditor.ITextEditorExtension3;
43 * Modifies <code>DocumentCommand</code>s inserting semicolons and opening
44 * braces to place them smartly, i.e. moving them to the end of a line if that
45 * is what the user expects.
48 * In practice, semicolons and braces (and the caret) are moved to the end of
49 * the line if they are typed anywhere except for semicolons in a
50 * <code>for</code> statements definition. If the line contains a semicolon or
51 * brace after the current caret position, the cursor is moved after it.
54 * @see org.eclipse.jface.text.DocumentCommand
57 public class SmartSemicolonAutoEditStrategy implements IAutoEditStrategy {
59 /** String representation of a semicolon. */
60 private static final String SEMICOLON = ";"; //$NON-NLS-1$
62 /** Char representation of a semicolon. */
63 private static final char SEMICHAR = ';';
65 /** String represenattion of a opening brace. */
66 private static final String BRACE = "{"; //$NON-NLS-1$
68 /** Char representation of a opening brace */
69 private static final char BRACECHAR = '{';
71 private char fCharacter;
73 private String fPartitioning;
76 * Creates a new SmartSemicolonAutoEditStrategy.
79 * the document partitioning
81 public SmartSemicolonAutoEditStrategy(String partitioning) {
82 fPartitioning = partitioning;
86 * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
87 * org.eclipse.jface.text.DocumentCommand)
89 public void customizeDocumentCommand(IDocument document,
90 DocumentCommand command) {
92 // also customize if <code>doit</code> is false (so it works in code
93 // completion situations)
97 if (command.text == null)
100 if (command.text.equals(SEMICOLON))
101 fCharacter = SEMICHAR;
102 else if (command.text.equals(BRACE))
103 fCharacter = BRACECHAR;
107 IPreferenceStore store = PHPeclipsePlugin.getDefault()
108 .getPreferenceStore();
109 if (fCharacter == SEMICHAR
111 .getBoolean(PreferenceConstants.EDITOR_SMART_SEMICOLON))
113 if (fCharacter == BRACECHAR
115 .getBoolean(PreferenceConstants.EDITOR_SMART_OPENING_BRACE))
118 IWorkbenchPage page = PHPeclipsePlugin.getActivePage();
121 IEditorPart part = page.getActiveEditor();
122 if (!(part instanceof PHPUnitEditor))
124 PHPUnitEditor editor = (PHPUnitEditor) part;
125 if (editor.getInsertMode() != ITextEditorExtension3.SMART_INSERT
126 || !editor.isEditable())
128 ITextEditorExtension2 extension = (ITextEditorExtension2) editor
129 .getAdapter(ITextEditorExtension2.class);
130 if (extension != null && !extension.validateEditorInputState())
132 if (isMultilineSelection(document, command))
135 // 1: find concerned line / position in java code, location in statement
136 int pos = command.offset;
139 IRegion l = document.getLineInformationOfOffset(pos);
140 line = new TextSelection(document, l.getOffset(), l.getLength());
141 } catch (BadLocationException e) {
145 // 2: choose action based on findings (is for-Statement?)
146 // for now: compute the best position to insert the new character
147 int positionInLine = computeCharacterPosition(document, line, pos
148 - line.getOffset(), fCharacter, fPartitioning);
149 int position = positionInLine + line.getOffset();
151 // never position before the current position!
155 // never double already existing content
156 if (alreadyPresent(document, fCharacter, position))
159 // don't do special processing if what we do is actually the normal
161 String insertion = adjustSpacing(document, position, fCharacter);
162 if (command.offset == position && insertion.equals(command.text))
167 final SmartBackspaceManager manager = (SmartBackspaceManager) editor
168 .getAdapter(SmartBackspaceManager.class);
170 && PHPeclipsePlugin.getDefault().getPreferenceStore()
172 PreferenceConstants.EDITOR_SMART_BACKSPACE)) {
173 TextEdit e1 = new ReplaceEdit(command.offset, command.text
174 .length(), document.get(command.offset, command.length));
175 UndoSpec s1 = new UndoSpec(command.offset
176 + command.text.length(), new Region(command.offset, 0),
177 new TextEdit[] { e1 }, 0, null);
179 DeleteEdit smart = new DeleteEdit(position, insertion.length());
180 ReplaceEdit raw = new ReplaceEdit(command.offset,
181 command.length, command.text);
182 UndoSpec s2 = new UndoSpec(position + insertion.length(),
183 new Region(command.offset + command.text.length(), 0),
184 new TextEdit[] { smart, raw }, 2, s1);
185 manager.register(s2);
189 command.offset = position;
191 command.caretOffset = position;
192 command.text = insertion;
194 command.owner = null;
195 } catch (MalformedTreeException e) {
196 PHPeclipsePlugin.log(e);
197 } catch (BadLocationException e) {
198 PHPeclipsePlugin.log(e);
204 * Returns <code>true</code> if the document command is applied on a multi
205 * line selection, <code>false</code> otherwise.
211 * @return <code>true</code> if <code>command</code> is a multiline
214 private boolean isMultilineSelection(IDocument document,
215 DocumentCommand command) {
217 return document.getNumberOfLines(command.offset, command.length) > 1;
218 } catch (BadLocationException e) {
225 * Adds a space before a brace if it is inserted after a parenthesis, equal
226 * sign, or one of the keywords <code>try, else, do</code>.
229 * the document we are working on
231 * the insert position of <code>character</code>
233 * the character to be inserted
234 * @return a <code>String</code> consisting of <code>character</code>
235 * plus any additional spacing
237 private String adjustSpacing(IDocument doc, int position, char character) {
238 if (character == BRACECHAR) {
239 if (position > 0 && position <= doc.getLength()) {
240 int pos = position - 1;
241 if (looksLike(doc, pos, ")") //$NON-NLS-1$
242 || looksLike(doc, pos, "=") //$NON-NLS-1$
243 || looksLike(doc, pos, "]") //$NON-NLS-1$
244 || looksLike(doc, pos, "try") //$NON-NLS-1$
245 || looksLike(doc, pos, "else") //$NON-NLS-1$
246 || looksLike(doc, pos, "synchronized") //$NON-NLS-1$
247 || looksLike(doc, pos, "static") //$NON-NLS-1$
248 || looksLike(doc, pos, "finally") //$NON-NLS-1$
249 || looksLike(doc, pos, "do")) //$NON-NLS-1$
250 return new String(new char[] { ' ', character });
254 return new String(new char[] { character });
258 * Checks whether a character to be inserted is already present at the
259 * insert location (perhaps separated by some whitespace from
260 * <code>position</code>.
263 * the document we are working on
265 * the insert position of <code>ch</code>
267 * the character to be inserted
268 * @return <code>true</code> if <code>ch</code> is already present at
269 * <code>location</code>, <code>false</code> otherwise
271 private boolean alreadyPresent(IDocument document, char ch, int position) {
272 int pos = firstNonWhitespaceForward(document, position, fPartitioning,
273 document.getLength());
275 if (pos != -1 && document.getChar(pos) == ch)
277 } catch (BadLocationException e) {
284 * Computes the next insert position of the given character in the current
288 * the document we are working on
290 * the line where the change is being made
292 * the position of the caret in the line when
293 * <code>character</code> was typed
295 * the character to look for
296 * @param partitioning
297 * the document partitioning
298 * @return the position where <code>character</code> should be inserted /
301 protected static int computeCharacterPosition(IDocument document,
302 ITextSelection line, int offset, char character, String partitioning) {
303 String text = line.getText();
308 if (character == BRACECHAR) {
310 insertPos = computeArrayInitializationPos(document, line, offset,
313 if (insertPos == -1) {
314 insertPos = computeAfterTryDoElse(document, line, offset);
317 if (insertPos == -1) {
318 insertPos = computeAfterParenthesis(document, line, offset,
322 } else if (character == SEMICHAR) {
324 if (isForStatement(text, offset)) {
325 insertPos = -1; // don't do anything in for statements, as semis
326 // are vital part of these
328 int nextPartitionPos = nextPartitionOrLineEnd(document, line,
329 offset, partitioning);
330 insertPos = startOfWhitespaceBeforeOffset(text,
332 // if there is a semi present, return its location as
333 // alreadyPresent() will take it out this way.
334 if (insertPos > 0 && text.charAt(insertPos - 1) == character)
335 insertPos = insertPos - 1;
339 Assert.isTrue(false);
347 * Computes an insert position for an opening brace if <code>offset</code>
348 * maps to a position in <code>document</code> that looks like being the
349 * RHS of an assignment or like an array definition.
352 * the document being modified
354 * the current line under investigation
356 * the offset of the caret position, relative to the line start.
357 * @param partitioning
358 * the document partitioning
359 * @return an insert position relative to the line start if
360 * <code>line</code> looks like being an array initialization at
361 * <code>offset</code>, -1 otherwise
363 private static int computeArrayInitializationPos(IDocument document,
364 ITextSelection line, int offset, String partitioning) {
365 // search backward while WS, find = (not != <= >= ==) in default
367 int pos = offset + line.getOffset();
372 int p = firstNonWhitespaceBackward(document, pos - 1, partitioning, -1);
379 char ch = document.getChar(p);
380 if (ch != '=' && ch != ']')
386 p = firstNonWhitespaceBackward(document, p - 1, partitioning, -1);
390 ch = document.getChar(p);
391 if (Scanner.isPHPIdentifierPart(ch) || ch == ']' || ch == '[')
394 } catch (BadLocationException e) {
400 * Computes an insert position for an opening brace if <code>offset</code>
401 * maps to a position in <code>document</code> involving a keyword taking
402 * a block after it. These are: <code>try</code>, <code>do</code>,
403 * <code>synchronized</code>, <code>static</code>,
404 * <code>finally</code>, or <code>else</code>.
407 * the document being modified
409 * the current line under investigation
411 * the offset of the caret position, relative to the line start.
412 * @return an insert position relative to the line start if
413 * <code>line</code> contains one of the above keywords at or
414 * before <code>offset</code>, -1 otherwise
416 private static int computeAfterTryDoElse(IDocument doc,
417 ITextSelection line, int offset) {
418 // search backward while WS, find 'try', 'do', 'else' in default
420 int p = offset + line.getOffset();
421 p = firstWhitespaceToRight(doc, p);
426 if (looksLike(doc, p, "try") //$NON-NLS-1$
427 || looksLike(doc, p, "do") //$NON-NLS-1$
428 || looksLike(doc, p, "synchronized") //$NON-NLS-1$
429 || looksLike(doc, p, "static") //$NON-NLS-1$
430 || looksLike(doc, p, "finally") //$NON-NLS-1$
431 || looksLike(doc, p, "else")) //$NON-NLS-1$
432 return p + 1 - line.getOffset();
438 * Computes an insert position for an opening brace if <code>offset</code>
439 * maps to a position in <code>document</code> with a expression in
440 * parenthesis that will take a block after the closing parenthesis.
443 * the document being modified
445 * the current line under investigation
447 * the offset of the caret position, relative to the line start.
448 * @param partitioning
449 * the document partitioning
450 * @return an insert position relative to the line start if
451 * <code>line</code> contains a parenthesized expression that can
452 * be followed by a block, -1 otherwise
454 private static int computeAfterParenthesis(IDocument document,
455 ITextSelection line, int offset, String partitioning) {
456 // find the opening parenthesis for every closing parenthesis on the
457 // current line after offset
458 // return the position behind the closing parenthesis if it looks like a
459 // method declaration
460 // or an expression for an if, while, for, catch statement
461 int pos = offset + line.getOffset();
462 int length = line.getOffset() + line.getLength();
463 int scanTo = scanForward(document, pos, partitioning, length, '}');
467 int closingParen = findClosingParenToLeft(document, pos, partitioning) - 1;
470 int startScan = closingParen + 1;
471 closingParen = scanForward(document, startScan, partitioning,
473 if (closingParen == -1)
476 int openingParen = findOpeningParenMatch(document, closingParen,
479 // no way an expression at the beginning of the document can mean
481 if (openingParen < 1)
484 // only select insert positions for parenthesis currently embracing
486 if (openingParen > pos)
489 if (looksLikeAnonymousClassDef(document, openingParen - 1,
491 return closingParen + 1 - line.getOffset();
493 if (looksLikeIfWhileForCatch(document, openingParen - 1,
495 return closingParen + 1 - line.getOffset();
497 if (looksLikeMethodDecl(document, openingParen - 1, partitioning))
498 return closingParen + 1 - line.getOffset();
506 * Finds a closing parenthesis to the left of <code>position</code> in
507 * document, where that parenthesis is only separated by whitespace from
508 * <code>position</code>. If no such parenthesis can be found,
509 * <code>position</code> is returned.
512 * the document being modified
514 * the first character position in <code>document</code> to be
516 * @param partitioning
517 * the document partitioning
518 * @return the position of a closing parenthesis left to
519 * <code>position</code> separated only by whitespace, or
520 * <code>position</code> if no parenthesis can be found
522 private static int findClosingParenToLeft(IDocument document, int position,
523 String partitioning) {
524 final char CLOSING_PAREN = ')';
529 int nonWS = firstNonWhitespaceBackward(document, position - 1,
531 if (nonWS != -1 && document.getChar(nonWS) == CLOSING_PAREN)
533 } catch (BadLocationException e1) {
539 * Finds the first whitespace character position to the right of (and
540 * including) <code>position</code>.
543 * the document being modified
545 * the first character position in <code>document</code> to be
547 * @return the position of a whitespace character greater or equal than
548 * <code>position</code> separated only by whitespace, or -1 if
551 private static int firstWhitespaceToRight(IDocument document, int position) {
552 int length = document.getLength();
553 Assert.isTrue(position >= 0);
554 Assert.isTrue(position <= length);
557 while (position < length) {
558 char ch = document.getChar(position);
559 if (Character.isWhitespace(ch))
564 } catch (BadLocationException e) {
570 * Finds the highest position in <code>document</code> such that the
571 * position is <= <code>position</code> and > <code>bound</code>
572 * and <code>Character.isWhitespace(document.getChar(pos))</code>
573 * evaluates to <code>false</code> and the position is in the default
577 * the document being modified
579 * the first character position in <code>document</code> to be
581 * @param partitioning
582 * the document partitioning
584 * the first position in <code>document</code> to not consider
585 * any more, with <code>bound</code> > <code>position</code>
586 * @return the highest position of one element in <code>chars</code> in [<code>position</code>,
587 * <code>scanTo</code>) that resides in a Java partition, or
588 * <code>-1</code> if none can be found
590 private static int firstNonWhitespaceBackward(IDocument document,
591 int position, String partitioning, int bound) {
592 Assert.isTrue(position < document.getLength());
593 Assert.isTrue(bound >= -1);
596 while (position > bound) {
597 char ch = document.getChar(position);
598 if (!Character.isWhitespace(ch)
599 && isDefaultPartition(document, position, partitioning))
603 } catch (BadLocationException e) {
609 * Finds the smallest position in <code>document</code> such that the
610 * position is >= <code>position</code> and < <code>bound</code>
611 * and <code>Character.isWhitespace(document.getChar(pos))</code>
612 * evaluates to <code>false</code> and the position is in the default
616 * the document being modified
618 * the first character position in <code>document</code> to be
620 * @param partitioning
621 * the document partitioning
623 * the first position in <code>document</code> to not consider
624 * any more, with <code>bound</code> > <code>position</code>
625 * @return the smallest position of one element in <code>chars</code> in [<code>position</code>,
626 * <code>scanTo</code>) that resides in a Java partition, or
627 * <code>-1</code> if none can be found
629 private static int firstNonWhitespaceForward(IDocument document,
630 int position, String partitioning, int bound) {
631 Assert.isTrue(position >= 0);
632 Assert.isTrue(bound <= document.getLength());
635 while (position < bound) {
636 char ch = document.getChar(position);
637 if (!Character.isWhitespace(ch)
638 && isDefaultPartition(document, position, partitioning))
642 } catch (BadLocationException e) {
648 * Finds the highest position in <code>document</code> such that the
649 * position is <= <code>position</code> and > <code>bound</code>
650 * and <code>document.getChar(position) == ch</code> evaluates to
651 * <code>true</code> for at least one ch in <code>chars</code> and the
652 * position is in the default partition.
655 * the document being modified
657 * the first character position in <code>document</code> to be
659 * @param partitioning
660 * the document partitioning
662 * the first position in <code>document</code> to not consider
663 * any more, with <code>scanTo</code> >
664 * <code>position</code>
666 * an array of <code>char</code> to search for
667 * @return the highest position of one element in <code>chars</code> in (<code>bound</code>,
668 * <code>position</code>] that resides in a Java partition, or
669 * <code>-1</code> if none can be found
671 private static int scanBackward(IDocument document, int position,
672 String partitioning, int bound, char[] chars) {
673 Assert.isTrue(bound >= -1);
674 Assert.isTrue(position < document.getLength());
679 while (position > bound) {
681 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
682 && isDefaultPartition(document, position, partitioning))
687 } catch (BadLocationException e) {
693 // * Finds the highest position in <code>document</code> such that the
694 // position is <= <code>position</code>
695 // * and > <code>bound</code> and <code>document.getChar(position) ==
696 // ch</code> evaluates to <code>true</code>
697 // * and the position is in the default partition.
699 // * @param document the document being modified
700 // * @param position the first character position in <code>document</code>
702 // * @param bound the first position in <code>document</code> to not
703 // consider any more, with <code>scanTo</code> > <code>position</code>
704 // * @param chars an array of <code>char</code> to search for
705 // * @return the highest position of one element in <code>chars</code> in
706 // [<code>position</code>, <code>scanTo</code>) that resides in a Java
707 // partition, or <code>-1</code> if none can be found
709 // private static int scanBackward(IDocument document, int position, int
711 // return scanBackward(document, position, bound, new char[] {ch});
715 * Finds the lowest position in <code>document</code> such that the
716 * position is >= <code>position</code> and < <code>bound</code>
717 * and <code>document.getChar(position) == ch</code> evaluates to
718 * <code>true</code> for at least one ch in <code>chars</code> and the
719 * position is in the default partition.
722 * the document being modified
724 * the first character position in <code>document</code> to be
726 * @param partitioning
727 * the document partitioning
729 * the first position in <code>document</code> to not consider
730 * any more, with <code>scanTo</code> >
731 * <code>position</code>
733 * an array of <code>char</code> to search for
734 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
735 * <code>bound</code>) that resides in a Java partition, or
736 * <code>-1</code> if none can be found
738 private static int scanForward(IDocument document, int position,
739 String partitioning, int bound, char[] chars) {
740 Assert.isTrue(position >= 0);
741 Assert.isTrue(bound <= document.getLength());
746 while (position < bound) {
748 if (Arrays.binarySearch(chars, document.getChar(position)) >= 0
749 && isDefaultPartition(document, position, partitioning))
754 } catch (BadLocationException e) {
760 * Finds the lowest position in <code>document</code> such that the
761 * position is >= <code>position</code> and < <code>bound</code>
762 * and <code>document.getChar(position) == ch</code> evaluates to
763 * <code>true</code> and the position is in the default partition.
766 * the document being modified
768 * the first character position in <code>document</code> to be
770 * @param partitioning
771 * the document partitioning
773 * the first position in <code>document</code> to not consider
774 * any more, with <code>scanTo</code> >
775 * <code>position</code>
777 * an array of <code>char</code> to search for
778 * @return the lowest position of one element in <code>chars</code> in [<code>position</code>,
779 * <code>bound</code>) that resides in a Java partition, or
780 * <code>-1</code> if none can be found
782 private static int scanForward(IDocument document, int position,
783 String partitioning, int bound, char ch) {
784 return scanForward(document, position, partitioning, bound,
789 * Checks whether the content of <code>document</code> in the range (<code>offset</code>,
790 * <code>length</code>) contains the <code>new</code> keyword.
793 * the document being modified
795 * the first character position in <code>document</code> to be
798 * the length of the character range to be considered
799 * @param partitioning
800 * the document partitioning
801 * @return <code>true</code> if the specified character range contains a
802 * <code>new</code> keyword, <code>false</code> otherwise.
804 private static boolean isNewMatch(IDocument document, int offset,
805 int length, String partitioning) {
806 Assert.isTrue(length >= 0);
807 Assert.isTrue(offset >= 0);
808 Assert.isTrue(offset + length < document.getLength() + 1);
811 String text = document.get(offset, length);
812 int pos = text.indexOf("new"); //$NON-NLS-1$
815 && !isDefaultPartition(document, pos + offset, partitioning))
816 pos = text.indexOf("new", pos + 2); //$NON-NLS-1$
821 if (pos != 0 && Scanner.isPHPIdentifierPart(text.charAt(pos - 1)))
825 && Scanner.isPHPIdentifierPart(text.charAt(pos + 3)))
830 } catch (BadLocationException e) {
836 * Checks whether the content of <code>document</code> at
837 * <code>position</code> looks like an anonymous class definition.
838 * <code>position</code> must be to the left of the opening parenthesis of
839 * the definition's parameter list.
842 * the document being modified
844 * the first character position in <code>document</code> to be
846 * @param partitioning
847 * the document partitioning
848 * @return <code>true</code> if the content of <code>document</code>
849 * looks like an anonymous class definition, <code>false</code>
852 private static boolean looksLikeAnonymousClassDef(IDocument document,
853 int position, String partitioning) {
854 int previousCommaOrParen = scanBackward(document, position - 1,
855 partitioning, -1, new char[] { ',', '(' });
856 if (previousCommaOrParen == -1 || position < previousCommaOrParen + 5) // 2
864 if (isNewMatch(document, previousCommaOrParen + 1, position
865 - previousCommaOrParen - 2, partitioning))
872 * Checks whether <code>position</code> resides in a default (Java)
873 * partition of <code>document</code>.
876 * the document being modified
878 * the position to be checked
879 * @param partitioning
880 * the document partitioning
881 * @return <code>true</code> if <code>position</code> is in the default
882 * partition of <code>document</code>, <code>false</code>
885 private static boolean isDefaultPartition(IDocument document, int position,
886 String partitioning) {
887 Assert.isTrue(position >= 0);
888 Assert.isTrue(position <= document.getLength());
891 // don't use getPartition2 since we're interested in the scanned
892 // character's partition
893 ITypedRegion region = TextUtilities.getPartition(document,
894 partitioning, position, false);
895 return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
897 } catch (BadLocationException e) {
904 * Finds the position of the parenthesis matching the closing parenthesis at
905 * <code>position</code>.
908 * the document being modified
910 * the position in <code>document</code> of a closing
912 * @param partitioning
913 * the document partitioning
914 * @return the position in <code>document</code> of the matching
915 * parenthesis, or -1 if none can be found
917 private static int findOpeningParenMatch(IDocument document, int position,
918 String partitioning) {
919 final char CLOSING_PAREN = ')';
920 final char OPENING_PAREN = '(';
922 Assert.isTrue(position < document.getLength());
923 Assert.isTrue(position >= 0);
924 Assert.isTrue(isDefaultPartition(document, position, partitioning));
928 Assert.isTrue(document.getChar(position) == CLOSING_PAREN);
932 position = scanBackward(document, position - 1, partitioning,
933 -1, new char[] { CLOSING_PAREN, OPENING_PAREN });
937 if (document.getChar(position) == CLOSING_PAREN)
946 } catch (BadLocationException e) {
952 * Checks whether, to the left of <code>position</code> and separated only
953 * by whitespace, <code>document</code> contains a keyword taking a
954 * parameter list and a block after it. These are: <code>if</code>,
955 * <code>while</code>, <code>catch</code>, <code>for</code>,
956 * <code>synchronized</code>, <code>switch</code>.
959 * the document being modified
961 * the first character position in <code>document</code> to be
963 * @param partitioning
964 * the document partitioning
965 * @return <code>true</code> if <code>document</code> contains any of
966 * the above keywords to the left of <code>position</code>,
967 * <code>false</code> otherwise
969 private static boolean looksLikeIfWhileForCatch(IDocument document,
970 int position, String partitioning) {
971 position = firstNonWhitespaceBackward(document, position, partitioning,
976 return looksLike(document, position, "if") //$NON-NLS-1$
977 || looksLike(document, position, "while") //$NON-NLS-1$
978 || looksLike(document, position, "catch") //$NON-NLS-1$
979 || looksLike(document, position, "synchronized") //$NON-NLS-1$
980 || looksLike(document, position, "switch") //$NON-NLS-1$
981 || looksLike(document, position, "for"); //$NON-NLS-1$
985 * Checks whether code>document</code> contains the <code>String</code> <code>like</code>
986 * such that its last character is at <code>position</code>. If <code>like</code>
987 * starts with a identifier part (as determined by
988 * {@link Scanner#isPHPIdentifierPart(char)}), it is also made sure that
989 * <code>like</code> is preceded by some non-identifier character or
990 * stands at the document start.
993 * the document being modified
995 * the first character position in <code>document</code> to be
998 * the <code>String</code> to look for.
999 * @return <code>true</code> if <code>document</code> contains <code>like</code>
1000 * such that it ends at <code>position</code>, <code>false</code>
1003 private static boolean looksLike(IDocument document, int position,
1005 int length = like.length();
1006 if (position < length - 1)
1010 if (!like.equals(document.get(position - length + 1, length)))
1013 if (position >= length
1014 && Scanner.isPHPIdentifierPart(like.charAt(0))
1015 && Scanner.isPHPIdentifierPart(document.getChar(position
1019 } catch (BadLocationException e) {
1027 * Checks whether the content of <code>document</code> at
1028 * <code>position</code> looks like a method declaration header (i.e. only
1029 * the return type and method name). <code>position</code> must be just
1030 * left of the opening parenthesis of the parameter list.
1033 * the document being modified
1035 * the first character position in <code>document</code> to be
1037 * @param partitioning
1038 * the document partitioning
1039 * @return <code>true</code> if the content of <code>document</code>
1040 * looks like a method definition, <code>false</code> otherwise
1042 private static boolean looksLikeMethodDecl(IDocument document,
1043 int position, String partitioning) {
1046 position = eatIdentToLeft(document, position, partitioning);
1050 position = eatBrackets(document, position - 1, partitioning);
1054 position = eatIdentToLeft(document, position - 1, partitioning);
1056 return position != -1;
1060 * From <code>position</code> to the left, eats any whitespace and then a
1061 * pair of brackets as used to declare an array return type like
1065 * </pre>. The return value is either the position of the opening bracket
1066 * or <code>position</code> if no pair of brackets can be parsed.
1069 * the document being modified
1071 * the first character position in <code>document</code> to be
1073 * @param partitioning
1074 * the document partitioning
1075 * @return the smallest character position of bracket pair or
1076 * <code>position</code>
1078 private static int eatBrackets(IDocument document, int position,
1079 String partitioning) {
1080 // accept array return type
1081 int pos = firstNonWhitespaceBackward(document, position, partitioning,
1084 if (pos > 1 && document.getChar(pos) == ']') {
1085 pos = firstNonWhitespaceBackward(document, pos - 1,
1087 if (pos > 0 && document.getChar(pos) == '[')
1090 } catch (BadLocationException e) {
1097 * From <code>position</code> to the left, eats any whitespace and the
1098 * first identifier, returning the position of the first identifier
1099 * character (in normal read order).
1101 * When called on a document with content <code>" some string "</code> and
1102 * positionition 13, the return value will be 6 (the first letter in
1103 * <code>string</code>).
1107 * the document being modified
1109 * the first character position in <code>document</code> to be
1111 * @param partitioning
1112 * the document partitioning
1113 * @return the smallest character position of an identifier or -1 if none
1114 * can be found; always <= <code>position</code>
1116 private static int eatIdentToLeft(IDocument document, int position,
1117 String partitioning) {
1120 Assert.isTrue(position < document.getLength());
1122 int p = firstNonWhitespaceBackward(document, position, partitioning, -1);
1129 char ch = document.getChar(p);
1130 if (Scanner.isPHPIdentifierPart(ch)) {
1135 // length must be > 0
1136 if (Character.isWhitespace(ch) && p != position)
1143 // start of document reached
1146 } catch (BadLocationException e) {
1152 * Returns a position in the first java partition after the last non-empty
1153 * and non-comment partition. There is no non-whitespace from the returned
1154 * position to the end of the partition it is contained in.
1157 * the document being modified
1159 * the line under investigation
1161 * the caret offset into <code>line</code>
1162 * @param partitioning
1163 * the document partitioning
1164 * @return the position of the next Java partition, or the end of
1167 private static int nextPartitionOrLineEnd(IDocument document,
1168 ITextSelection line, int offset, String partitioning) {
1169 // run relative to document
1170 final int docOffset = offset + line.getOffset();
1171 final int eol = line.getOffset() + line.getLength();
1172 int nextPartitionPos = eol; // init with line end
1173 int validPosition = docOffset;
1176 ITypedRegion partition = TextUtilities.getPartition(document,
1177 partitioning, nextPartitionPos, true);
1178 validPosition = getValidPositionForPartition(document, partition,
1180 while (validPosition == -1) {
1181 nextPartitionPos = partition.getOffset() - 1;
1182 if (nextPartitionPos < docOffset) {
1183 validPosition = docOffset;
1186 partition = TextUtilities.getPartition(document, partitioning,
1187 nextPartitionPos, false);
1188 validPosition = getValidPositionForPartition(document,
1191 } catch (BadLocationException e) {
1194 validPosition = Math.max(validPosition, docOffset);
1195 // make relative to line
1196 validPosition -= line.getOffset();
1197 return validPosition;
1201 * Returns a valid insert location (except for whitespace) in
1202 * <code>partition</code> or -1 if there is no valid insert location. An
1203 * valid insert location is right after any java string or character
1204 * partition, or at the end of a java default partition, but never behind
1205 * <code>maxOffset</code>. Comment partitions or empty java partitions do
1206 * never yield valid insert positions.
1209 * the document being modified
1211 * the current partition
1213 * the maximum offset to consider
1214 * @return a valid insert location in <code>partition</code>, or -1 if
1215 * there is no valid insert location
1217 private static int getValidPositionForPartition(IDocument doc,
1218 ITypedRegion partition, int maxOffset) {
1219 final int INVALID = -1;
1221 if (IPHPPartitions.PHP_PHPDOC_COMMENT.equals(partition.getType()))
1223 if (IPHPPartitions.PHP_MULTILINE_COMMENT.equals(partition.getType()))
1225 if (IPHPPartitions.PHP_SINGLELINE_COMMENT.equals(partition.getType()))
1228 int endOffset = Math.min(maxOffset, partition.getOffset()
1229 + partition.getLength());
1231 // if (IPHPPartitions.JAVA_CHARACTER.equals(partition.getType()))
1232 // return endOffset;
1233 if (IPHPPartitions.PHP_STRING_DQ.equals(partition.getType()))
1235 if (IPHPPartitions.PHP_STRING_SQ.equals(partition.getType()))
1237 if (IPHPPartitions.PHP_STRING_HEREDOC.equals(partition.getType()))
1239 if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) {
1241 if (doc.get(partition.getOffset(),
1242 endOffset - partition.getOffset()).trim().length() == 0)
1246 } catch (BadLocationException e) {
1250 // default: we don't know anything about the partition - assume valid
1255 * Determines whether the current line contains a for statement. Algorithm:
1256 * any "for" word in the line is a positive, "for" contained in a string
1257 * literal will produce a false positive.
1260 * the line where the change is being made
1262 * the position of the caret
1263 * @return <code>true</code> if <code>line</code> contains
1264 * <code>for</code>, <code>false</code> otherwise
1266 private static boolean isForStatement(String line, int offset) {
1267 /* searching for (^|\s)for(\s|$) */
1268 int forPos = line.indexOf("for"); //$NON-NLS-1$
1270 if ((forPos == 0 || !Scanner.isPHPIdentifierPart(line
1271 .charAt(forPos - 1)))
1272 && (line.length() == forPos + 3 || !Scanner
1273 .isPHPIdentifierPart(line.charAt(forPos + 3))))
1280 * Returns the position in <code>text</code> after which there comes only
1281 * whitespace, up to <code>offset</code>.
1284 * the text being searched
1286 * the maximum offset to search for
1287 * @return the smallest value <code>v</code> such that
1288 * <code>text.substring(v, offset).trim() == 0</code>
1290 private static int startOfWhitespaceBeforeOffset(String text, int offset) {
1291 int i = Math.min(offset, text.length());
1292 for (; i >= 1; i--) {
1293 if (!Character.isWhitespace(text.charAt(i - 1)))