blob: c31b59b0a13727953b7ab48115052bd98e7acc2f [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.plugins.groovy.editor.actions;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorModificationUtil;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.text.CharArrayCharSequence;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyFileType;
import org.jetbrains.plugins.groovy.codeStyle.GroovyCodeStyleSettings;
import org.jetbrains.plugins.groovy.editor.HandlerUtils;
import org.jetbrains.plugins.groovy.formatter.GeeseUtil;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyLexer;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection;
import org.jetbrains.plugins.groovy.lang.psi.util.GrStringUtil;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mCLOSABLE_BLOCK_OP;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mDOLLAR;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mDOLLAR_SLASH_REGEX_BEGIN;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mDOLLAR_SLASH_REGEX_CONTENT;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mDOLLAR_SLASH_REGEX_END;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mDOLLAR_SLASH_REGEX_LITERAL;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mGSTRING_BEGIN;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mGSTRING_CONTENT;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mGSTRING_END;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mGSTRING_LITERAL;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mIDENT;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mLBRACK;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mLCURLY;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mNLS;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mRBRACK;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mRCURLY;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mREGEX_BEGIN;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mREGEX_CONTENT;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mREGEX_END;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mREGEX_LITERAL;
import static org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes.mSTRING_LITERAL;
import static org.jetbrains.plugins.groovy.lang.parser.GroovyElementTypes.*;
/**
* @author ilyas
*/
public class GroovyEnterHandler extends EnterHandlerDelegateAdapter {
private static final TokenSet GSTRING_TOKENS = TokenSet.create(mGSTRING_BEGIN, mGSTRING_CONTENT, mGSTRING_END, mGSTRING_LITERAL);
private static final TokenSet REGEX_TOKENS = TokenSet.create(mREGEX_BEGIN, mREGEX_CONTENT, mREGEX_END, mDOLLAR_SLASH_REGEX_BEGIN,
mDOLLAR_SLASH_REGEX_CONTENT, mDOLLAR_SLASH_REGEX_END);
private static final TokenSet AFTER_DOLLAR = TokenSet.create(mLCURLY, mIDENT, mDOLLAR, mGSTRING_END, mREGEX_END, mDOLLAR_SLASH_REGEX_END,
GSTRING_CONTENT, mGSTRING_CONTENT, mREGEX_CONTENT, mDOLLAR_SLASH_REGEX_CONTENT);
private static final TokenSet ALL_STRINGS = TokenSet.create(mSTRING_LITERAL, mGSTRING_LITERAL, mGSTRING_BEGIN, mGSTRING_END,
mGSTRING_CONTENT, mRCURLY, mIDENT, mDOLLAR, mREGEX_BEGIN, mREGEX_CONTENT,
mREGEX_END, mDOLLAR_SLASH_REGEX_BEGIN, mDOLLAR_SLASH_REGEX_CONTENT,
mDOLLAR_SLASH_REGEX_END, mREGEX_LITERAL, mDOLLAR_SLASH_REGEX_LITERAL, GSTRING_CONTENT);
private static final TokenSet BEFORE_DOLLAR =TokenSet.create(mGSTRING_BEGIN, mREGEX_BEGIN, mDOLLAR_SLASH_REGEX_BEGIN, GSTRING_CONTENT,
mGSTRING_CONTENT, mREGEX_CONTENT, mDOLLAR_SLASH_REGEX_CONTENT);
private static final TokenSet EXPR_END = TokenSet.create(mRCURLY, mIDENT);
private static final TokenSet AFTER_EXPR_END = TokenSet.create(mGSTRING_END, mDOLLAR, mREGEX_END, mDOLLAR_SLASH_REGEX_END, GSTRING_CONTENT,
mGSTRING_CONTENT, mREGEX_CONTENT, mDOLLAR_SLASH_REGEX_CONTENT);
private static final TokenSet STRING_END = TokenSet.create(mSTRING_LITERAL, mGSTRING_LITERAL, mGSTRING_END, mREGEX_END,
mDOLLAR_SLASH_REGEX_END, mREGEX_LITERAL, mDOLLAR_SLASH_REGEX_LITERAL);
private static final TokenSet INNER_STRING_TOKENS = TokenSet.create(mGSTRING_BEGIN, mGSTRING_CONTENT, mGSTRING_END, mREGEX_BEGIN,
mREGEX_CONTENT, mREGEX_END, mDOLLAR_SLASH_REGEX_BEGIN,
mDOLLAR_SLASH_REGEX_CONTENT, mDOLLAR_SLASH_REGEX_END,
GSTRING_INJECTION, GSTRING_CONTENT);
public static void insertSpacesByGroovyContinuationIndent(Editor editor, Project project) {
int indentSize = CodeStyleSettingsManager.getSettings(project).getContinuationIndentSize(GroovyFileType.GROOVY_FILE_TYPE);
EditorModificationUtil.insertStringAtCaret(editor, StringUtil.repeatSymbol(' ', indentSize));
}
public Result preprocessEnter(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull Ref<Integer> caretOffset,
@NotNull Ref<Integer> caretAdvance,
@NotNull DataContext dataContext,
EditorActionHandler originalHandler) {
Document document = editor.getDocument();
Project project = file.getProject();
CaretModel caretModel = editor.getCaretModel();
String text = document.getText();
if (StringUtil.isEmpty(text)) {
return Result.Continue;
}
if (!(file instanceof GroovyFileBase)) {
return Result.Continue;
}
final int caret = caretModel.getOffset();
final EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
if (caret >= 1 && caret < text.length() && CodeInsightSettings.getInstance().SMART_INDENT_ON_ENTER) {
HighlighterIterator iterator = highlighter.createIterator(caret);
iterator.retreat();
while (!iterator.atEnd() && TokenType.WHITE_SPACE == iterator.getTokenType()) {
iterator.retreat();
}
boolean afterArrow = !iterator.atEnd() && iterator.getTokenType() == mCLOSABLE_BLOCK_OP;
if (afterArrow) {
originalHandler.execute(editor, dataContext);
PsiDocumentManager.getInstance(project).commitDocument(document);
CodeStyleManager.getInstance(project).adjustLineIndent(file, caretModel.getOffset());
}
iterator = highlighter.createIterator(caretModel.getOffset());
while (!iterator.atEnd() && TokenType.WHITE_SPACE == iterator.getTokenType()) {
iterator.advance();
}
if (!iterator.atEnd() && mRCURLY == iterator.getTokenType()) {
PsiDocumentManager.getInstance(project).commitDocument(document);
final PsiElement element = file.findElementAt(iterator.getStart());
if (element != null &&
element.getNode().getElementType() == mRCURLY &&
element.getParent() instanceof GrClosableBlock &&
text.length() > caret && afterArrow) {
return Result.DefaultForceIndent;
}
}
if (afterArrow) {
return Result.Stop;
}
if (editor.isInsertMode() &&
!HandlerUtils.isReadOnly(editor) &&
!editor.getSelectionModel().hasSelection() &&
handleFlyingGeese(editor, caret, dataContext, originalHandler, file)) {
return Result.DefaultForceIndent;
}
}
if (handleEnter(editor, dataContext, project, originalHandler)) return Result.Stop;
return Result.Continue;
}
protected static boolean handleEnter(Editor editor,
DataContext dataContext,
@NotNull Project project,
EditorActionHandler originalHandler) {
if (HandlerUtils.isReadOnly(editor)) {
return false;
}
int caretOffset = editor.getCaretModel().getOffset();
if (caretOffset < 1) return false;
if (handleBetweenSquareBraces(editor, caretOffset, dataContext, project, originalHandler)) {
return true;
}
if (handleInString(editor, caretOffset, dataContext, originalHandler)) {
return true;
}
return false;
}
private static boolean handleFlyingGeese(Editor editor,
int caretOffset,
DataContext dataContext,
EditorActionHandler originalHandler,
PsiFile file) {
Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project == null) return false;
GroovyCodeStyleSettings codeStyleSettings =
CodeStyleSettingsManager.getSettings(project).getCustomSettings(GroovyCodeStyleSettings.class);
if (!codeStyleSettings.USE_FLYING_GEESE_BRACES) return false;
PsiElement element = file.findElementAt(caretOffset);
if (element != null && element.getNode().getElementType() == TokenType.WHITE_SPACE) {
element = GeeseUtil.getNextNonWhitespaceToken(element);
}
if (element == null || !GeeseUtil.isClosureRBrace(element)) return false;
element = GeeseUtil.getNextNonWhitespaceToken(element);
if (element == null || element.getNode().getElementType() != mNLS || StringUtil.countChars(element.getText(), '\n') > 1) {
return false;
}
element = GeeseUtil.getNextNonWhitespaceToken(element);
if (element == null || !GeeseUtil.isClosureRBrace(element)) return false;
Document document = editor.getDocument();
PsiDocumentManager.getInstance(project).commitDocument(document);
int toRemove = element.getTextRange().getStartOffset();
document.deleteString(caretOffset + 1, toRemove);
originalHandler.execute(editor, dataContext);
String text = document.getText();
int nextLineFeed = text.indexOf('\n', caretOffset + 1);
if (nextLineFeed == -1) nextLineFeed = text.length();
CodeStyleManager.getInstance(project).reformatText(file, caretOffset, nextLineFeed);
return true;
}
private static boolean handleBetweenSquareBraces(Editor editor,
int caret,
DataContext context,
Project project,
EditorActionHandler originalHandler) {
String text = editor.getDocument().getText();
if (text == null || text.length() == 0) return false;
final EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
if (caret < 1 || caret > text.length() - 1) {
return false;
}
HighlighterIterator iterator = highlighter.createIterator(caret - 1);
if (mLBRACK == iterator.getTokenType()) {
if (text.length() > caret) {
iterator = highlighter.createIterator(caret);
if (mRBRACK == iterator.getTokenType()) {
originalHandler.execute(editor, context);
originalHandler.execute(editor, context);
editor.getCaretModel().moveCaretRelatively(0, -1, false, false, true);
insertSpacesByGroovyContinuationIndent(editor, project);
return true;
}
}
}
return false;
}
private static boolean handleInString(Editor editor, int caretOffset, DataContext dataContext, EditorActionHandler originalHandler) {
Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project == null) return false;
final VirtualFile vfile = FileDocumentManager.getInstance().getFile(editor.getDocument());
assert vfile != null;
PsiFile file = PsiManager.getInstance(project).findFile(vfile);
Document document = editor.getDocument();
String fileText = document.getText();
if (fileText.length() == caretOffset) return false;
if (!checkStringApplicable(editor, caretOffset)) return false;
if (file == null) return false;
PsiDocumentManager.getInstance(project).commitDocument(document);
final PsiElement stringElement = inferStringPair(file, caretOffset);
if (stringElement == null) return false;
ASTNode node = stringElement.getNode();
final IElementType nodeElementType = node.getElementType();
boolean isInsertIndent = isInsertIndent(caretOffset, stringElement.getTextRange().getStartOffset(), fileText);
// For simple String literals like 'abcdef'
CaretModel caretModel = editor.getCaretModel();
if (nodeElementType == mSTRING_LITERAL) {
if (isSingleQuoteString(stringElement)) {
//the case of print '\<caret>'
if (isSlashBeforeCaret(caretOffset, fileText)) {
EditorModificationUtil.insertStringAtCaret(editor, "\n");
}
else if(stringElement.getParent() instanceof GrReferenceExpression) {
TextRange range = stringElement.getTextRange();
convertEndToMultiline(range.getEndOffset(), document, fileText, '\'');
document.insertString(range.getStartOffset(), "''");
caretModel.moveToOffset(caretOffset + 2);
EditorModificationUtil.insertStringAtCaret(editor, "\n");
}
else {
EditorModificationUtil.insertStringAtCaret(editor, "'+");
originalHandler.execute(editor, dataContext);
EditorModificationUtil.insertStringAtCaret(editor, "'");
PsiDocumentManager.getInstance(project).commitDocument(document);
CodeStyleManager.getInstance(project).reformatRange(file, caretOffset, caretModel.getOffset());
}
}
else {
insertLineFeedInString(editor, dataContext, originalHandler, isInsertIndent);
}
return true;
}
if (GSTRING_TOKENS.contains(nodeElementType) ||
nodeElementType == GSTRING_CONTENT && GSTRING_TOKENS.contains(node.getFirstChildNode().getElementType()) ||
nodeElementType == mDOLLAR && node.getTreeParent().getTreeParent().getElementType() == GSTRING) {
PsiElement parent = stringElement.getParent();
if (nodeElementType == mGSTRING_LITERAL) {
parent = stringElement;
}
else {
while (parent != null && !(parent instanceof GrLiteral)) {
parent = parent.getParent();
}
}
if (parent == null) return false;
if (isDoubleQuotedString(parent)) {
PsiElement exprSibling = stringElement.getNextSibling();
boolean rightFromDollar = exprSibling instanceof GrExpression && exprSibling.getTextRange().getStartOffset() == caretOffset;
if (rightFromDollar) caretOffset--;
TextRange parentRange = parent.getTextRange();
if (rightFromDollar || parent.getParent() instanceof GrReferenceExpression) {
convertEndToMultiline(parent.getTextRange().getEndOffset(), document, fileText, '"');
document.insertString(parentRange.getStartOffset(), "\"\"");
caretModel.moveToOffset(caretOffset + 2);
EditorModificationUtil.insertStringAtCaret(editor, "\n");
if (rightFromDollar) {
caretModel.moveCaretRelatively(1, 0, false, false, true);
}
}
else if (isSlashBeforeCaret(caretOffset, fileText)) {
EditorModificationUtil.insertStringAtCaret(editor, "\n");
}
else {
EditorModificationUtil.insertStringAtCaret(editor, "\"+");
originalHandler.execute(editor, dataContext);
EditorModificationUtil.insertStringAtCaret(editor, "\"");
PsiDocumentManager.getInstance(project).commitDocument(document);
CodeStyleManager.getInstance(project).reformatRange(file, caretOffset, caretModel.getOffset());
}
}
else {
insertLineFeedInString(editor, dataContext, originalHandler, isInsertIndent);
}
return true;
}
if (REGEX_TOKENS.contains(nodeElementType) ||
nodeElementType == GSTRING_CONTENT && REGEX_TOKENS.contains(node.getFirstChildNode().getElementType()) ||
nodeElementType == mDOLLAR && node.getTreeParent().getTreeParent().getElementType() == REGEX) {
PsiElement parent = stringElement.getParent();
if (nodeElementType == mREGEX_LITERAL || nodeElementType == mDOLLAR_SLASH_REGEX_LITERAL) {
parent = stringElement;
}
else {
while (parent != null && !(parent instanceof GrLiteral)) {
parent = parent.getParent();
}
}
if (parent == null || parent.getLastChild() instanceof PsiErrorElement) return false;
PsiElement exprSibling = stringElement.getNextSibling();
boolean rightFromDollar = exprSibling instanceof GrExpression && exprSibling.getTextRange().getStartOffset() == caretOffset;
if (rightFromDollar) {
caretModel.moveToOffset(caretOffset - 1);
}
insertLineFeedInString(editor, dataContext, originalHandler, isInsertIndent);
if (rightFromDollar) {
caretModel.moveCaretRelatively(1, 0, false, false, true);
}
return true;
}
return false;
}
private static boolean isDoubleQuotedString(PsiElement element) {
return "\"".equals(GrStringUtil.getStartQuote(element.getText()));
}
private static boolean isSingleQuoteString(PsiElement element) {
return "'".equals(GrStringUtil.getStartQuote(element.getText()));
}
@Nullable
private static PsiElement inferStringPair(PsiFile file, int caretOffset) {
PsiElement stringElement = file.findElementAt(caretOffset - 1);
if (stringElement == null) return null;
ASTNode node = stringElement.getNode();
if (node == null) return null;
// For expression injection in GString like "abc ${}<caret> abc"
if (!INNER_STRING_TOKENS.contains(node.getElementType()) && checkGStringInjection(stringElement)) {
stringElement = stringElement.getParent().getParent().getNextSibling();
if (stringElement == null) return null;
}
return stringElement;
}
private static boolean isSlashBeforeCaret(int caretOffset, String fileText) {
return caretOffset > 0 && fileText.charAt(caretOffset - 1) == '\\';
}
private static void insertLineFeedInString(Editor editor,
DataContext dataContext,
EditorActionHandler originalHandler,
boolean isInsertIndent) {
if (isInsertIndent) {
originalHandler.execute(editor, dataContext);
}
else {
EditorModificationUtil.insertStringAtCaret(editor, "\n");
}
}
private static boolean isInsertIndent(int caret, int stringOffset, String text) {
final int i = text.indexOf('\n', stringOffset);
return stringOffset < i && i < caret;
}
private static void convertEndToMultiline(int caretOffset, Document document, String fileText, char c) {
if (caretOffset < fileText.length() && fileText.charAt(caretOffset) == c ||
caretOffset > 0 && fileText.charAt(caretOffset - 1) == c) {
document.insertString(caretOffset, new CharArrayCharSequence(c, c));
}
else {
document.insertString(caretOffset, new CharArrayCharSequence(c, c, c));
}
}
private static boolean checkStringApplicable(Editor editor, int caret) {
final GroovyLexer lexer = new GroovyLexer();
lexer.start(editor.getDocument().getText());
while (lexer.getTokenEnd() < caret) {
lexer.advance();
}
final IElementType leftToken = lexer.getTokenType();
if (lexer.getTokenEnd() <= caret) lexer.advance();
final IElementType rightToken = lexer.getTokenType();
if (!(ALL_STRINGS.contains(leftToken))) {
return false;
}
if (BEFORE_DOLLAR.contains(leftToken) && !AFTER_DOLLAR.contains(rightToken)) {
return false;
}
if (EXPR_END.contains(leftToken) && !AFTER_EXPR_END.contains(rightToken)) {
return false;
}
if (STRING_END.contains(leftToken) && !STRING_END.contains(rightToken)) {
return false;
}
return true;
}
private static boolean checkGStringInjection(PsiElement element) {
if (element != null && (element.getParent() instanceof GrReferenceExpression || element.getParent() instanceof GrClosableBlock)) {
final PsiElement parent = element.getParent().getParent();
if (!(parent instanceof GrStringInjection)) return false;
PsiElement nextSibling = parent.getNextSibling();
if (nextSibling == null) return false;
return INNER_STRING_TOKENS.contains(nextSibling.getNode().getElementType());
}
return false;
}
}