in php/php.editor/src/org/netbeans/modules/php/editor/typinghooks/PhpTypedTextInterceptor.java [76:610]
public void insert(MutableContext context) throws BadLocationException {
isAfter = false;
codeTemplateEditing = false;
bracketCompleted = false;
Document document = context.getDocument();
BaseDocument doc = (BaseDocument) document;
int caretOffset = context.getOffset();
char ch = context.getText().charAt(0);
if (doNotAutoComplete(ch) || caretOffset == 0) {
return;
}
String selection = context.getReplacedText();
if (selection != null && selection.length() > 0) {
codeTemplateEditing = GsfUtilities.isCodeTemplateEditing(doc);
if (!codeTemplateEditing && (ch == '"' || ch == '\'' || ch == '(' || ch == '{' || ch == '[')) {
char firstChar = selection.charAt(0);
if (firstChar != ch) {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPositionedSequence(doc, caretOffset);
if (ts != null && (!TypingHooksUtils.isStringToken(ts.token()) || firstChar == '\"' || firstChar == '\'')) {
int lastChar = selection.charAt(selection.length() - 1);
// Replace the surround-with chars?
if (selection.length() > 1
&& ((firstChar == '"' || firstChar == '\'' || firstChar == '('
|| firstChar == '{' || firstChar == '[')
&& lastChar == matching(firstChar))) {
String innerText = selection.substring(1, selection.length() - 1);
String text = Character.toString(ch) + innerText + Character.toString(matching(ch));
context.setText(text, text.length());
bracketCompleted = true;
} else if (selection.length() == 1 && (firstChar == '"' || firstChar == '\'')) {
if (ts.token().id() == PHPTokenId.PHP_CONSTANT_ENCAPSED_STRING) {
String original = ts.token().text().toString();
if (original.length() > 1) {
String text = ch + original.substring(1, original.length() - 1) + ch;
doc.remove(ts.offset(), text.length());
context.setText(text, text.length() - 1);
} else {
return;
}
}
} else {
String text = ch + selection + matching(ch);
context.setText(text, text.length());
bracketCompleted = true;
}
return;
}
}
}
}
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, caretOffset);
if (ts == null) {
return;
}
ts.move(caretOffset);
if (!ts.moveNext() && !ts.movePrevious()) {
return;
}
Token<? extends PHPTokenId> token = ts.token();
TokenId id = token.id();
if (id == PHPTokenId.PHP_LINE_COMMENT && selection != null && selection.length() > 0) {
if (ch == '*' || ch == '+' || ch == '_') {
if (selection.charAt(0) != ch && selection.indexOf(' ') == -1) {
String text = ch + selection + matching(ch);
context.setText(text, text.length());
return;
}
}
}
if (ch == '"' || ch == '\'') {
completeQuote(context);
}
}
@Override
public void afterInsert(final Context context) throws BadLocationException {
final BaseDocument doc = (BaseDocument) context.getDocument();
doc.runAtomicAsUser(new Runnable() {
@Override
public void run() {
try {
afterInsertUnderWriteLock(context);
} catch (BadLocationException ex) {
LOGGER.log(Level.FINE, null, ex);
}
}
});
}
private void afterInsertUnderWriteLock(Context context) throws BadLocationException {
isAfter = true;
JTextComponent target = context.getComponent();
Caret caret = target.getCaret();
BaseDocument doc = (BaseDocument) context.getDocument();
int dotPos = context.getOffset();
char ch = context.getText().charAt(0);
// See if our automatic adjustment of indentation when typing (for example) "end" was
// premature - if you were typing a longer word beginning with one of my adjustment
// prefixes, such as "endian", then put the indentation back.
if (previousAdjustmentOffset != -1) {
if (dotPos == previousAdjustmentOffset) {
// Revert indentation iff the character at the insert position does
// not start a new token (e.g. the previous token that we reindented
// was not complete)
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, dotPos);
if (ts != null) {
ts.move(dotPos);
if (ts.moveNext() && (ts.offset() < dotPos)) {
GsfUtilities.setLineIndentation((Document) doc, dotPos, previousAdjustmentIndent);
}
}
}
previousAdjustmentOffset = -1;
}
switch (ch) {
case '}':
case '{':
case ')':
case ']':
case '(':
case '[':
case '\t':
case ' ':
case ':':
if (!TypingHooksUtils.isInsertMatchingEnabled() && ch != '{' && ch != '}') {
return;
}
Token<? extends PHPTokenId> token = LexUtilities.getToken(doc, dotPos);
if (token == null) {
return;
}
TokenId id = token.id();
if (((id == PHPTokenId.PHP_VARIABLE) && (token.length() == 1))
|| (LexUtilities.textEquals(token.text(), '[')) || (LexUtilities.textEquals(token.text(), ']'))
|| (LexUtilities.textEquals(token.text(), '(')) || (LexUtilities.textEquals(token.text(), ')'))
|| id == PHPTokenId.PHP_ATTRIBUTE /* #[ */) {
if (ch == ']' || ch == ')') {
skipClosingBracket(doc, caret, ch);
} else if ((ch == '[') || (ch == '(')) {
completeOpeningBracket(doc, dotPos, caret, ch);
}
} else if (id == PHPTokenId.PHP_CASTING && ch == ')') {
skipClosingBracket(doc, caret, ch);
}
// Reindent blocks (won't do anything if } is not at the beginning of a line
if (ch == '}') {
reindent(doc, dotPos, PHPTokenId.PHP_CURLY_CLOSE, caret);
} else if (ch == '{') {
reindent(doc, dotPos, PHPTokenId.PHP_CURLY_OPEN, caret);
} else if (ch == '\t' || ch == ' ') {
reindent(doc, dotPos, PHPTokenId.WHITESPACE, caret);
} else if (ch == ':') {
reindent(doc, dotPos, PHPTokenId.PHP_TOKEN, caret);
}
break;
default:
//no-op
}
}
@Override
public void cancelled(Context context) {
}
private boolean doNotAutoComplete(final char ch) {
return (!TypingHooksUtils.isInsertMatchingEnabled() && isBracket(ch)) || (isQuote(ch) && !OptionsUtils.autoCompletionSmartQuotes());
}
private boolean isBracket(final char ch) {
return isOpeningBracket(ch) || isClosingBracket(ch);
}
private boolean isOpeningBracket(final char ch) {
return ch == '(' || ch == '{' || ch == '[';
}
private boolean isClosingBracket(final char ch) {
return ch == ')' || ch == '}' || ch == ']';
}
private boolean isQuote(final char ch) {
return ch == '"' || ch == '\'';
}
private boolean isQuote(final Token<? extends PHPTokenId> token) {
return isQuote(token.text().charAt(0));
}
/**
* Returns for an opening bracket or quote the appropriate closing
* character.
*/
private char matching(char bracket) {
switch (bracket) {
case '(':
return ')';
case '/':
return '/';
case '[':
return ']';
case '\"':
return '\"'; // NOI18N
case '\'':
return '\'';
case '{':
return '}';
case '}':
return '{';
default:
return bracket;
}
}
/**
* Check for conditions and possibly complete an already inserted quote .
*/
private void completeQuote(MutableContext context) throws BadLocationException {
int dotPos = context.getOffset();
BaseDocument doc = (BaseDocument) context.getDocument();
char bracket = context.getText().charAt(0);
if (codeTemplateEditing) {
String text = context.getText() + bracket;
context.setText(text, text.length() - 1);
return;
}
// No chars completion when escaping, eg \" or \' typed
if (isEscapeSequence(doc, dotPos)) {
return;
}
// Find the token sequence and look at what token is under the caret
Object[] result = findPhpSectionBoundaries(doc, dotPos, true);
if (result == null) {
// not in PHP section
return;
}
@SuppressWarnings("unchecked")
TokenSequence<? extends PHPTokenId> ts = (TokenSequence<? extends PHPTokenId>) result[0];
int sectionEnd = (Integer) result[2];
boolean onlyWhitespaceFollows = (Boolean) result[4];
Token<? extends PHPTokenId> token = ts.token();
if (token == null) { // Issue #151886
return;
}
Token<? extends PHPTokenId> previousToken = ts.movePrevious() ? ts.token() : null;
// Check if we are inside a comment
if (token.id() == PHPTokenId.PHP_COMMENT
|| token.id() == PHPTokenId.PHP_LINE_COMMENT
|| token.id() == PHPTokenId.PHPDOC_COMMENT
|| token.id() == PHPTokenId.T_INLINE_HTML // #132981
) {
return;
}
// Check if we are inside a string
boolean insideString = TypingHooksUtils.isStringToken(token)/* || context.getText().startsWith("\"") || context.getText().startsWith("'")*/; //NOI18N
if (!insideString) {
if (onlyWhitespaceFollows && previousToken != null && TypingHooksUtils.isStringToken(previousToken)) {
// The same as for the line comment above. We could be at the EOL
// of a string literal, token is the EOL whitespace,
// but the previous token is PHP string
insideString = true;
}
}
if (insideString) {
if (!onlyWhitespaceFollows) {
//#69524
char chr = doc.getChars(dotPos, 1)[0];
if (chr == bracket) {
if (!isAfter) {
String text = "" + bracket;
context.setText(text, text.length());
}
doc.remove(dotPos, 1);
}
}
} else {
boolean insert = onlyWhitespaceFollows;
if (!insert) {
int firstNonWhiteFwd = LineDocumentUtils.getNextNonWhitespace(doc, dotPos, sectionEnd);
if (firstNonWhiteFwd != -1) {
char chr = doc.getChars(firstNonWhiteFwd, 1)[0];
insert = (chr == ')' || chr == ',' || chr == '+' || chr == '}' || //NOI18N
chr == ';' || chr == ']' || chr == '.') && !TypingHooksUtils.isStringToken(previousToken) && !isQuote(token); //NOI18N
}
}
if (insert) {
String text = "" + bracket + (isAfter ? "" : matching(bracket));
context.setText(text, isAfter ? text.length() : text.length() - 1);
}
}
}
private boolean isEscapeSequence(BaseDocument doc, int dotPos) throws BadLocationException {
if (dotPos <= 0) {
return false;
}
char previousChar = doc.getChars(dotPos - 1, 1)[0];
return previousChar == '\\';
}
private static Object[] findPhpSectionBoundaries(BaseDocument doc, int offset, boolean currentLineOnly) {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, offset);
if (ts == null) {
return null;
}
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return null;
}
// determine the row boundaries
int lowest = 0;
int highest = doc.getLength();
if (currentLineOnly) {
lowest = doc.getParagraphElement(offset).getStartOffset();
highest = Math.max(doc.getParagraphElement(offset).getEndOffset() - 1, lowest);
}
// find the section end
int sectionEnd = highest;
boolean onlyWhitespaceFollows = true;
do {
if (highest < ts.offset()) {
break;
}
if (ts.token().id() == PHPTokenId.PHP_CLOSETAG) {
sectionEnd = ts.offset();
break;
} else if (ts.token().id() != PHPTokenId.WHITESPACE) {
onlyWhitespaceFollows = false;
}
} while (ts.moveNext());
// find the section start
int sectionStart = lowest;
boolean onlyWhitespacePreceeds = true;
while (ts.movePrevious()) {
if (lowest > ts.offset()) {
break;
}
if (ts.token().id() == PHPTokenId.PHP_OPENTAG) {
sectionStart = ts.offset();
break;
} else if (ts.token().id() != PHPTokenId.WHITESPACE) {
onlyWhitespacePreceeds = false;
}
}
// re-position the sequence
ts.move(offset);
if (!ts.moveNext()) {
assert ts.movePrevious();
}
assert sectionStart != -1 && sectionEnd != -1 : "sectionStart=" + sectionStart + ", sectionEnd=" + sectionEnd; //NOI18N
return new Object[]{ts, sectionStart, sectionEnd, onlyWhitespacePreceeds, onlyWhitespaceFollows};
}
private void reindent(BaseDocument doc, int offset, TokenId id, Caret caret) throws BadLocationException {
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, offset);
if (ts != null) {
ts.move(offset);
if (!ts.moveNext() && !ts.movePrevious()) {
return;
}
Token<? extends PHPTokenId> token = ts.token();
if ((token.id() == id)) {
final int rowFirstNonWhite = Utilities.getRowFirstNonWhite(doc, offset);
if (id == PHPTokenId.PHP_CURLY_OPEN && ts.offset() == rowFirstNonWhite
&& ts.movePrevious()) {
// The curly is at the first nonwhite char at the line.
// Do we need to indent the { according previous line?
int previousExprestion = LexUtilities.findStartTokenOfExpression(ts);
int previousIndent = Utilities.getRowIndent(doc, previousExprestion);
int currentIndent = Utilities.getRowIndent(doc, offset);
int newIndent = IndentUtils.countIndent(doc, offset, previousIndent);
if (newIndent != currentIndent) {
GsfUtilities.setLineIndentation((Document) doc, offset, Math.max(newIndent, 0));
}
} else if (id == PHPTokenId.WHITESPACE || (id == PHPTokenId.PHP_TOKEN && token.text().charAt(0) == ':')) { // ":" handles "default:"
Token<? extends PHPTokenId> previousToken = null;
if (id == PHPTokenId.WHITESPACE) {
previousToken = LexUtilities.findPreviousToken(ts, Arrays.asList(PHPTokenId.PHP_CASE, PHPTokenId.PHP_TOKEN));
} else {
if (ts.movePrevious()) { // ":"
previousToken = LexUtilities.findPreviousToken(ts, Arrays.asList(PHPTokenId.PHP_DEFAULT, PHPTokenId.PHP_TOKEN));
}
}
if (ts.offset() >= rowFirstNonWhite
&& previousToken != null
&& (previousToken.id() == PHPTokenId.PHP_CASE
|| previousToken.id() == PHPTokenId.PHP_DEFAULT)) {
// previous "case" or "default" on one line with typed char
previousToken = LexUtilities.findPreviousToken(ts, Arrays.asList(PHPTokenId.PHP_SWITCH));
if (previousToken != null && previousToken.id() == PHPTokenId.PHP_SWITCH) {
Token<? extends PHPTokenId> firstCaseInSwitch = LexUtilities.findNextToken(ts, Arrays.asList(PHPTokenId.PHP_CASE));
if (firstCaseInSwitch != null && firstCaseInSwitch.id() == PHPTokenId.PHP_CASE) {
int indentOfFirstCase = GsfUtilities.getLineIndent((Document) doc, ts.offset());
GsfUtilities.setLineIndentation((Document) doc, offset, indentOfFirstCase);
}
}
}
} else if (id == PHPTokenId.PHP_CURLY_CLOSE) {
OffsetRange begin = LexUtilities.findBwd(doc, ts, PHPTokenId.PHP_CURLY_OPEN, '{', PHPTokenId.PHP_CURLY_CLOSE, '}');
if (begin != OffsetRange.NONE) {
int beginOffset = begin.getStart();
int indent = GsfUtilities.getLineIndent((Document) doc, beginOffset);
previousAdjustmentIndent = GsfUtilities.getLineIndent((Document) doc, offset);
GsfUtilities.setLineIndentation((Document) doc, offset, indent);
previousAdjustmentOffset = caret.getDot();
}
}
}
}
}
/**
* A hook to be called after closing bracket ) or ] was inserted into the
* document. The method checks if the bracket should stay there or be
* removed and some exisitng bracket just skipped.
*
* @param doc the document
* @param dotPos position of the inserted bracket
* @param caret caret
* @param bracket the bracket character ']' or ')'
*/
private void skipClosingBracket(BaseDocument doc, Caret caret, char bracket) throws BadLocationException {
int caretOffset = caret.getDot();
if (isSkipClosingBracket(doc, caretOffset, bracket)) {
doc.remove(caretOffset - 1, 1);
caret.setDot(caretOffset); // skip closing bracket
}
}
/**
* Check whether the typed bracket should stay in the document or be
* removed. <br> This method is called by
* <code>skipClosingBracket()</code>.
*
* @param doc document into which typing was done.
* @param caretOffset
*/
private boolean isSkipClosingBracket(BaseDocument doc, int caretOffset, char bracket) throws BadLocationException {
if (caretOffset == doc.getLength()) {
return false;
}
TokenSequence<? extends PHPTokenId> ts = LexUtilities.getPHPTokenSequence(doc, caretOffset);
if (ts == null) {
return false;
}
ts.move(caretOffset);
if (!ts.moveNext()) {
return false;
}
Token<? extends PHPTokenId> token = ts.token();
boolean skipClosingBracket = false;
// Check whether character follows the bracket is the same bracket
if ((token != null) && (LexUtilities.textEquals(token.text(), bracket))) {
char leftBracket = bracket == ')' ? '(' : (bracket == ']' ? '[' : '{');
int bracketBalanceWithNewBracket = 0;
ts.moveStart();
if (!ts.moveNext()) {
return false;
}
token = ts.token();
while (token != null) {
// GH-6706 we can get "[" as PHP_ENCAPSED_AND_WHITESPACE token e.g. $x = "[$y example]"
if (token.id() == PHPTokenId.PHP_TOKEN) {
if ((LexUtilities.textEquals(token.text(), '(')) || (LexUtilities.textEquals(token.text(), '['))) {
if (LexUtilities.textEquals(token.text(), leftBracket)) {
bracketBalanceWithNewBracket++;
}
} else if ((LexUtilities.textEquals(token.text(), ')')) || (LexUtilities.textEquals(token.text(), ']'))) {
if (LexUtilities.textEquals(token.text(), bracket)) {
bracketBalanceWithNewBracket--;
}
}
}
if (!ts.moveNext()) {
break;
}
token = ts.token();
}
skipClosingBracket = bracketBalanceWithNewBracket != 0;
}
return skipClosingBracket;
}
/**
* Check for various conditions and possibly add a pairing bracket to the
* already inserted.
*
* @param doc the document
* @param dotPos position of the opening bracket (already in the doc)
* @param caret caret
* @param bracket the bracket that was inserted
*/
private void completeOpeningBracket(BaseDocument doc, int dotPos, Caret caret, char bracket) throws BadLocationException {
if (!bracketCompleted && isCompletablePosition(doc, dotPos + 1)) {
String matchingBracket = "" + matching(bracket);
doc.insertString(dotPos + 1, matchingBracket, null);
caret.setDot(dotPos + 1);
}
}
/**
* Checks whether dotPos is a position at which bracket and quote completion
* is performed. Brackets and quotes are not completed everywhere but just
* at suitable places .
*
* @param doc the document
* @param dotPos position to be tested
*/
private boolean isCompletablePosition(BaseDocument doc, int dotPos) throws BadLocationException {
if (dotPos == doc.getLength()) { // there's no other character to test
return true;
} else {
// test that we are in front of ) , " or '
char chr = doc.getChars(dotPos, 1)[0];
return ((chr == ')') || (chr == ',') || (chr == '\"') || (chr == '\'') || (chr == ' ')
|| (chr == ']') || (chr == '}') || (chr == '\n') || (chr == '\t') || (chr == ';'));
}
}
@MimeRegistration(mimeType = FileUtils.PHP_MIME_TYPE, service = TypedTextInterceptor.Factory.class)
public static class Factory implements TypedTextInterceptor.Factory {
@Override
public TypedTextInterceptor createTypedTextInterceptor(MimePath mimePath) {
return new PhpTypedTextInterceptor();
}
}
}