blob: d0a97c7a3d59e85773870d9cc89d8a39b18cebdc [file] [log] [blame]
/*
* Copyright 2000-2014 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 com.jetbrains.python.editor;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.editorActions.AutoHardWrapHandler;
import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
import com.intellij.ide.DataManager;
import com.intellij.injected.editor.EditorWindow;
import com.intellij.lang.ASTNode;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
import com.intellij.openapi.editor.actions.SplitLineAction;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.codeInsight.PyCodeInsightSettings;
import com.jetbrains.python.documentation.PythonDocumentationProvider;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author yole
*/
public class PythonEnterHandler extends EnterHandlerDelegateAdapter {
private int myPostprocessShift = 0;
public static final Class[] IMPLICIT_WRAP_CLASSES = new Class[] {
PySequenceExpression.class,
PyDictLiteralExpression.class,
PyParenthesizedExpression.class,
PyArgumentList.class,
PyParameterList.class
};
private static final Class[] WRAPPABLE_CLASSES = new Class[]{
PsiComment.class,
PyParenthesizedExpression.class,
PyListCompExpression.class,
PyDictCompExpression.class,
PySetCompExpression.class,
PyDictLiteralExpression.class,
PySetLiteralExpression.class,
PyListLiteralExpression.class,
PyArgumentList.class,
PyParameterList.class,
PyFunction.class,
PySliceExpression.class,
PySubscriptionExpression.class,
PyGeneratorExpression.class
};
@Override
public Result preprocessEnter(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull Ref<Integer> caretOffset,
@NotNull Ref<Integer> caretAdvance,
@NotNull DataContext dataContext,
EditorActionHandler originalHandler) {
int offset = caretOffset.get();
if (editor instanceof EditorWindow) {
file = InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file);
editor = InjectedLanguageUtil.getTopLevelEditor(editor);
offset = editor.getCaretModel().getOffset();
}
if (!(file instanceof PyFile)) {
return Result.Continue;
}
final Boolean isSplitLine = DataManager.getInstance().loadFromDataContext(dataContext, SplitLineAction.SPLIT_LINE_KEY);
if (isSplitLine != null) {
return Result.Continue;
}
final Document doc = editor.getDocument();
PsiDocumentManager.getInstance(file.getProject()).commitDocument(doc);
final PsiElement element = file.findElementAt(offset);
CodeInsightSettings codeInsightSettings = CodeInsightSettings.getInstance();
if (codeInsightSettings.JAVADOC_STUB_ON_ENTER) {
PsiElement comment = element;
if (comment == null && offset != 0) {
comment = file.findElementAt(offset - 1);
}
int expectedStringStart = editor.getCaretModel().getOffset() - 3; // """ or '''
if (PythonDocCommentUtil.atDocCommentStart(comment, expectedStringStart)) {
insertDocStringStub(editor, comment);
return Result.Continue;
}
}
if (element == null) {
return Result.Continue;
}
PsiElement elementParent = element.getParent();
if (element.getNode().getElementType() == PyTokenTypes.LPAR) elementParent = elementParent.getParent();
if (elementParent instanceof PyParenthesizedExpression || elementParent instanceof PyGeneratorExpression) return Result.Continue;
if (offset > 0 && !(PyTokenTypes.STRING_NODES.contains(element.getNode().getElementType()))) {
final PsiElement prevElement = file.findElementAt(offset - 1);
if (prevElement == element) return Result.Continue;
}
if (PyTokenTypes.TRIPLE_NODES.contains(element.getNode().getElementType()) ||
element.getNode().getElementType() == PyTokenTypes.DOCSTRING) {
return Result.Continue;
}
final PsiElement prevElement = file.findElementAt(offset - 1);
PyStringLiteralExpression string = PsiTreeUtil.findElementOfClassAtOffset(file, offset, PyStringLiteralExpression.class, false);
if (string != null && prevElement != null && PyTokenTypes.STRING_NODES.contains(prevElement.getNode().getElementType())
&& string.getTextOffset() < offset && !(element.getNode() instanceof PsiWhiteSpace)) {
final String stringText = element.getText();
final int prefixLength = PyStringLiteralExpressionImpl.getPrefixLength(stringText);
if (string.getTextOffset() + prefixLength >= offset) {
return Result.Continue;
}
final String pref = element.getText().substring(0, prefixLength);
final String quote = element.getText().substring(prefixLength, prefixLength + 1);
final boolean nextIsBackslash = "\\".equals(doc.getText(TextRange.create(offset - 1, offset)));
final boolean isEscapedQuote = quote.equals(doc.getText(TextRange.create(offset, offset + 1))) && nextIsBackslash;
final boolean isEscapedBackslash = "\\".equals(doc.getText(TextRange.create(offset-2, offset - 1))) && nextIsBackslash;
if (nextIsBackslash && !isEscapedQuote && !isEscapedBackslash) return Result.Continue;
final StringBuilder replacementString = new StringBuilder();
myPostprocessShift = prefixLength + quote.length();
if (PsiTreeUtil.getParentOfType(string, IMPLICIT_WRAP_CLASSES) != null) {
replacementString.append(quote).append(pref).append(quote);
doc.insertString(offset, replacementString);
caretOffset.set(caretOffset.get() + 1);
return Result.Continue;
}
else {
if (isEscapedQuote) {
replacementString.append(quote);
caretOffset.set(caretOffset.get() + 1);
}
replacementString.append(quote).append(" \\").append(pref);
if (!isEscapedQuote)
replacementString.append(quote);
doc.insertString(offset, replacementString.toString());
caretOffset.set(caretOffset.get() + 3);
return Result.Continue;
}
}
if (!PyCodeInsightSettings.getInstance().INSERT_BACKSLASH_ON_WRAP) {
return Result.Continue;
}
return checkInsertBackslash(file, caretOffset, dataContext, offset, doc);
}
private static Result checkInsertBackslash(PsiFile file,
Ref<Integer> caretOffset,
DataContext dataContext,
int offset,
Document doc) {
boolean autoWrapInProgress = DataManager.getInstance().loadFromDataContext(dataContext,
AutoHardWrapHandler.AUTO_WRAP_LINE_IN_PROGRESS_KEY) != null;
if (needInsertBackslash(file, offset, autoWrapInProgress)) {
doc.insertString(offset, "\\");
caretOffset.set(caretOffset.get() + 1);
}
return Result.Continue;
}
public static boolean needInsertBackslash(PsiFile file, int offset, boolean autoWrapInProgress) {
if (offset > 0) {
final PsiElement beforeCaret = file.findElementAt(offset - 1);
if (beforeCaret instanceof PsiWhiteSpace && beforeCaret.getText().indexOf('\\') >= 0) {
// we've got a backslash at EOL already, don't need another one
return false;
}
}
PsiElement atCaret = file.findElementAt(offset);
if (atCaret == null) {
return false;
}
ASTNode nodeAtCaret = atCaret.getNode();
return needInsertBackslash(nodeAtCaret, autoWrapInProgress);
}
public static boolean needInsertBackslash(ASTNode nodeAtCaret, boolean autoWrapInProgress) {
PsiElement statementBefore = findStatementBeforeCaret(nodeAtCaret);
PsiElement statementAfter = findStatementAfterCaret(nodeAtCaret);
if (statementBefore != statementAfter) { // Enter pressed at statement break
return false;
}
if (statementBefore == null) { // empty file
return false;
}
if (PsiTreeUtil.hasErrorElements(statementBefore)) {
if (!autoWrapInProgress) {
// code is already bad, don't mess it up even further
return false;
}
// if we're in middle of typing, it's expected that we will have error elements
}
if (inFromImportParentheses(statementBefore, nodeAtCaret.getTextRange().getStartOffset())) {
return false;
}
PsiElement wrappableBefore = findWrappable(nodeAtCaret, true);
PsiElement wrappableAfter = findWrappable(nodeAtCaret, false);
if (!(wrappableBefore instanceof PsiComment)) {
while (wrappableBefore != null) {
PsiElement next = PsiTreeUtil.getParentOfType(wrappableBefore, WRAPPABLE_CLASSES);
if (next == null) {
break;
}
wrappableBefore = next;
}
}
if (!(wrappableAfter instanceof PsiComment)) {
while (wrappableAfter != null) {
PsiElement next = PsiTreeUtil.getParentOfType(wrappableAfter, WRAPPABLE_CLASSES);
if (next == null) {
break;
}
wrappableAfter = next;
}
}
if (wrappableBefore instanceof PsiComment || wrappableAfter instanceof PsiComment) {
return false;
}
return wrappableAfter == null || wrappableBefore != wrappableAfter;
}
private static void insertDocStringStub(Editor editor, PsiElement element) {
PythonDocumentationProvider provider = new PythonDocumentationProvider();
PyFunction fun = PsiTreeUtil.getParentOfType(element, PyFunction.class);
if (fun != null) {
String docStub = provider.generateDocumentationContentStub(fun, false);
docStub += element.getParent().getText().substring(0,3);
if (docStub.length() != 0) {
editor.getDocument().insertString(editor.getCaretModel().getOffset(), docStub);
return;
}
}
PyElement klass = PsiTreeUtil.getParentOfType(element, PyClass.class, PyFile.class);
if (klass != null && element != null) {
editor.getDocument().insertString(editor.getCaretModel().getOffset(),
PythonDocCommentUtil.generateDocForClass(klass, element.getParent().getText().substring(0, 3)));
}
}
@Nullable
private static PsiElement findWrappable(ASTNode nodeAtCaret, boolean before) {
PsiElement wrappable = before
? findBeforeCaret(nodeAtCaret, WRAPPABLE_CLASSES)
: findAfterCaret(nodeAtCaret, WRAPPABLE_CLASSES);
if (wrappable == null) {
PsiElement emptyTuple = before
? findBeforeCaret(nodeAtCaret, PyTupleExpression.class)
: findAfterCaret(nodeAtCaret, PyTupleExpression.class);
if (emptyTuple != null && emptyTuple.getNode().getFirstChildNode().getElementType() == PyTokenTypes.LPAR) {
wrappable = emptyTuple;
}
}
return wrappable;
}
@Nullable
private static PsiElement findStatementBeforeCaret(ASTNode node) {
return findBeforeCaret(node, PyStatement.class);
}
@Nullable
private static PsiElement findStatementAfterCaret(ASTNode node) {
return findAfterCaret(node, PyStatement.class);
}
private static PsiElement findBeforeCaret(ASTNode atCaret, Class<? extends PsiElement>... classes) {
while (atCaret != null) {
atCaret = TreeUtil.prevLeaf(atCaret);
if (atCaret != null && atCaret.getElementType() != TokenType.WHITE_SPACE) {
return getNonStrictParentOfType(atCaret.getPsi(), classes);
}
}
return null;
}
private static PsiElement findAfterCaret(ASTNode atCaret, Class<? extends PsiElement>... classes) {
while (atCaret != null) {
if (atCaret.getElementType() != TokenType.WHITE_SPACE) {
return getNonStrictParentOfType(atCaret.getPsi(), classes);
}
atCaret = TreeUtil.nextLeaf(atCaret);
}
return null;
}
@Nullable
private static <T extends PsiElement> T getNonStrictParentOfType(@NotNull PsiElement element, @NotNull Class<? extends T>... classes) {
PsiElement run = element;
while (run != null) {
for (Class<? extends T> aClass : classes) {
if (aClass.isInstance(run)) return (T)run;
}
if (run instanceof PsiFile || run instanceof PyStatementList) break;
run = run.getParent();
}
return null;
}
private static boolean inFromImportParentheses(PsiElement statement, int offset) {
if (!(statement instanceof PyFromImportStatement)) {
return false;
}
PyFromImportStatement fromImportStatement = (PyFromImportStatement)statement;
PsiElement leftParen = fromImportStatement.getLeftParen();
if (leftParen != null && offset >= leftParen.getTextRange().getEndOffset()) {
return true;
}
return false;
}
@Override
public Result postProcessEnter(@NotNull PsiFile file,
@NotNull Editor editor,
@NotNull DataContext dataContext) {
if (myPostprocessShift > 0) {
editor.getCaretModel().moveCaretRelatively(myPostprocessShift, 0, false, false, false);
myPostprocessShift = 0;
}
return super.postProcessEnter(file, editor,
dataContext);
}
}