/*
* Copyright 2010-2015 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.kotlin.idea.editor;
import com.intellij.codeInsight.AutoPopupController;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
import com.intellij.codeInsight.highlighting.BraceMatcher;
import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorModificationUtil;
import com.intellij.openapi.editor.ScrollType;
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.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.formatter.FormatterUtil;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.KtNodeTypes;
import org.jetbrains.kotlin.lexer.KtTokens;
import org.jetbrains.kotlin.psi.KtClassOrObject;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.psi.KtQualifiedExpression;
import org.jetbrains.kotlin.psi.KtSimpleNameStringTemplateEntry;
public class KotlinTypedHandler extends TypedHandlerDelegate {
private final static TokenSet CONTROL_FLOW_EXPRESSIONS = TokenSet.create(
KtNodeTypes.IF,
KtNodeTypes.ELSE,
KtNodeTypes.FOR,
KtNodeTypes.WHILE,
KtNodeTypes.TRY);
private final static TokenSet SUPPRESS_AUTO_INSERT_CLOSE_BRACE_AFTER = TokenSet.create(
KtTokens.RPAR,
KtTokens.ELSE_KEYWORD,
KtTokens.TRY_KEYWORD
);
private boolean kotlinLTTyped;
@Override
public Result beforeCharTyped(char c, Project project, Editor editor, PsiFile file, FileType fileType) {
if (!(file instanceof KtFile)) {
return Result.CONTINUE;
}
switch (c) {
case '<':
kotlinLTTyped = CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET &&
LtGtTypingUtils.shouldAutoCloseAngleBracket(editor.getCaretModel().getOffset(), editor);
autoPopupParameterInfo(project, editor);
return Result.CONTINUE;
case '>':
if (CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET) {
if (LtGtTypingUtils.handleKotlinGTInsert(editor)) {
return Result.STOP;
}
}
return Result.CONTINUE;
case '{':
// Returning Result.CONTINUE will cause inserting "{}" for unmatched '{'
int offset = editor.getCaretModel().getOffset();
if (offset == 0) {
return Result.CONTINUE;
}
HighlighterIterator iterator = ((EditorEx) editor).getHighlighter().createIterator(offset - 1);
while (!iterator.atEnd() && iterator.getTokenType() == TokenType.WHITE_SPACE) {
iterator.retreat();
}
if (iterator.atEnd() || !(SUPPRESS_AUTO_INSERT_CLOSE_BRACE_AFTER.contains(iterator.getTokenType()))) {
return Result.CONTINUE;
}
int tokenBeforeBraceOffset = iterator.getStart();
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
PsiElement leaf = file.findElementAt(offset);
if (leaf != null) {
PsiElement parent = leaf.getParent();
if (parent != null && CONTROL_FLOW_EXPRESSIONS.contains(parent.getNode().getElementType())) {
ASTNode nonWhitespaceSibling = FormatterUtil.getPreviousNonWhitespaceSibling(leaf.getNode());
if (nonWhitespaceSibling != null && nonWhitespaceSibling.getStartOffset() == tokenBeforeBraceOffset) {
EditorModificationUtil.insertStringAtCaret(editor, "{", false, true);
indentBrace(project, editor, '{');
return Result.STOP;
}
}
}
return Result.CONTINUE;
case '.':
autoPopupMemberLookup(project, editor);
return Result.CONTINUE;
case '@':
autoPopupLabelLookup(project, editor);
return Result.CONTINUE;
case ':':
autoPopupCallableReferenceLookup(project, editor);
return Result.CONTINUE;
case '[':
autoPopupParameterInfo(project, editor);
return Result.CONTINUE;
}
return Result.CONTINUE;
}
private static void autoPopupParameterInfo(Project project, Editor editor) {
int offset = editor.getCaretModel().getOffset();
if (offset == 0) return;
HighlighterIterator iterator = ((EditorEx) editor).getHighlighter().createIterator(offset - 1);
IElementType tokenType = iterator.getTokenType();
if (KtTokens.COMMENTS.contains(tokenType)
|| tokenType == KtTokens.REGULAR_STRING_PART
|| tokenType == KtTokens.OPEN_QUOTE
|| tokenType == KtTokens.CHARACTER_LITERAL) return;
AutoPopupController.getInstance(project).autoPopupParameterInfo(editor, null);
}
private static void autoPopupMemberLookup(Project project, final Editor editor) {
AutoPopupController.getInstance(project).autoPopupMemberLookup(editor, new Condition<PsiFile>() {
@Override
public boolean value(PsiFile file) {
int offset = editor.getCaretModel().getOffset();
PsiElement lastToken = file.findElementAt(offset - 1);
if (lastToken == null) return false;
IElementType elementType = lastToken.getNode().getElementType();
if (elementType == KtTokens.DOT || elementType == KtTokens.SAFE_ACCESS) return true;
if (elementType == KtTokens.REGULAR_STRING_PART && lastToken.getTextRange().getStartOffset() == offset - 1) {
PsiElement prevSibling = lastToken.getParent().getPrevSibling();
return prevSibling != null && prevSibling instanceof KtSimpleNameStringTemplateEntry;
}
return false;
}
});
}
private static void autoPopupLabelLookup(Project project, final Editor editor) {
AutoPopupController.getInstance(project).autoPopupMemberLookup(editor, new Condition<PsiFile>() {
@Override
public boolean value(PsiFile file) {
int offset = editor.getCaretModel().getOffset();
CharSequence chars = editor.getDocument().getCharsSequence();
if (!endsWith(chars, offset, "this@")
&& !endsWith(chars, offset, "return@")
&& !endsWith(chars, offset, "break@")
&& !endsWith(chars, offset, "continue@")) return false;
PsiElement lastElement = file.findElementAt(offset - 1);
if (lastElement == null) return false;
return lastElement.getNode().getElementType() == KtTokens.AT;
}
});
}
private static void autoPopupCallableReferenceLookup(Project project, final Editor editor) {
AutoPopupController.getInstance(project).autoPopupMemberLookup(editor, new Condition<PsiFile>() {
@Override
public boolean value(PsiFile file) {
int offset = editor.getCaretModel().getOffset();
PsiElement lastElement = file.findElementAt(offset - 1);
if (lastElement == null) return false;
return lastElement.getNode().getElementType() == KtTokens.COLONCOLON;
}
});
}
private static boolean endsWith(CharSequence chars, int offset, String text) {
if (offset < text.length()) return false;
return chars.subSequence(offset - text.length(), offset).toString().equals(text);
}
@Override
public Result charTyped(char c, Project project, @NotNull Editor editor, @NotNull PsiFile file) {
if (!(file instanceof KtFile)) {
return Result.CONTINUE;
}
if (kotlinLTTyped) {
kotlinLTTyped = false;
LtGtTypingUtils.handleKotlinAutoCloseLT(editor);
return Result.STOP;
}
else if (c == '{' && CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET) {
PsiDocumentManager.getInstance(project).commitAllDocuments();
int offset = editor.getCaretModel().getOffset();
PsiElement previousElement = file.findElementAt(offset - 1);
if (previousElement instanceof LeafPsiElement
&& ((LeafPsiElement) previousElement).getElementType() == KtTokens.LONG_TEMPLATE_ENTRY_START) {
editor.getDocument().insertString(offset, "}");
return Result.STOP;
}
}
else if (c == ':') {
if (autoIndentCase(editor, project, file, KtClassOrObject.class)) {
return Result.STOP;
}
}
else if (c == '.') {
if (autoIndentCase(editor, project, file, KtQualifiedExpression.class)) {
return Result.STOP;
}
}
return Result.CONTINUE;
}
/**
* Copied from
* @see com.intellij.codeInsight.editorActions.TypedHandler#indentBrace(Project, Editor, char)
*/
private static void indentBrace(@NotNull final Project project, @NotNull final Editor editor, char braceChar) {
final int offset = editor.getCaretModel().getOffset() - 1;
Document document = editor.getDocument();
CharSequence chars = document.getCharsSequence();
if (offset < 0 || chars.charAt(offset) != braceChar) return;
int spaceStart = CharArrayUtil.shiftBackward(chars, offset - 1, " \t");
if (spaceStart < 0 || chars.charAt(spaceStart) == '\n' || chars.charAt(spaceStart) == '\r') {
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
documentManager.commitDocument(document);
final PsiFile file = documentManager.getPsiFile(document);
if (file == null || !file.isWritable()) return;
PsiElement element = file.findElementAt(offset);
if (element == null) return;
EditorHighlighter highlighter = ((EditorEx) editor).getHighlighter();
HighlighterIterator iterator = highlighter.createIterator(offset);
FileType fileType = file.getFileType();
BraceMatcher braceMatcher = BraceMatchingUtil.getBraceMatcher(fileType, iterator);
boolean isBrace =
braceMatcher.isLBraceToken(iterator, chars, fileType) || braceMatcher.isRBraceToken(iterator, chars, fileType);
if (element.getNode() != null && isBrace) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
int newOffset = CodeStyleManager.getInstance(project).adjustLineIndent(file, offset);
editor.getCaretModel().moveToOffset(newOffset + 1);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
}
});
}
}
}
private static boolean autoIndentCase(Editor editor, Project project, PsiFile file, Class<?> kclass) {
int offset = editor.getCaretModel().getOffset();
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
PsiElement currElement = file.findElementAt(offset - 1);
if (currElement != null) {
// Should be applied only if there's nothing but the whitespace in line before the element
PsiElement prevLeaf = PsiTreeUtil.prevLeaf(currElement);
if (!(prevLeaf instanceof PsiWhiteSpace && prevLeaf.getText().contains("\n"))) {
return false;
}
PsiElement parent = currElement.getParent();
if (parent != null && kclass.isInstance(parent)) {
int curElementLength = currElement.getText().length();
if (offset < curElementLength) return false;
CodeStyleManager.getInstance(project).adjustLineIndent(file, offset - curElementLength);
return true;
}
}
return false;
}
}