blob: 7ac39f60f5b04a8c022b0300162c4b5d2575dd38 [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 com.jetbrains.python.editor;
import com.intellij.codeInsight.editorActions.emacs.EmacsProcessingHandler;
import com.intellij.formatting.IndentInfo;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.SelectionModel;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.DocumentUtil;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyTokenTypes;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* Python-specific Emacs-like processing extension.
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since 4/11/11 2:31 PM
*/
public class PyEmacsHandler implements EmacsProcessingHandler {
private static final TokenSet COMPOUND_STATEMENT_TYPES = TokenSet.create(
PyElementTypes.IF_STATEMENT, PyTokenTypes.IF_KEYWORD, PyTokenTypes.ELIF_KEYWORD, PyTokenTypes.ELSE_KEYWORD,
PyElementTypes.WHILE_STATEMENT, PyTokenTypes.WHILE_KEYWORD,
PyElementTypes.FOR_STATEMENT, PyTokenTypes.FOR_KEYWORD,
PyElementTypes.WITH_STATEMENT, PyTokenTypes.WITH_KEYWORD,
PyElementTypes.TRY_EXCEPT_STATEMENT, PyTokenTypes.TRY_KEYWORD, PyTokenTypes.EXCEPT_KEYWORD, PyTokenTypes.FINALLY_KEYWORD,
PyElementTypes.FUNCTION_DECLARATION, PyTokenTypes.DEF_KEYWORD,
PyElementTypes.CLASS_DECLARATION, PyTokenTypes.CLASS_KEYWORD
);
private enum ProcessingResult {
/** Particular sub-routine did the job and no additional processing is necessary. */
STOP_SUCCESSFUL,
/** Particular sub-routine detected that the processing should be stopped. */
STOP_UNSUCCESSFUL,
/** Processing should be continued */
CONTINUE
}
/**
* Tries to make active line(s) belong to another code block by changing their indentation.
*
* @param project current project
* @param editor current editor
* @param file current file
* @return {@link Result#STOP} if indentation level is changed and further processing should be stopped;
* {@link Result#CONTINUE} otherwise
*/
@NotNull
@Override
public Result changeIndent(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
// The algorithm is as follows:
// 1. Check if the editor has selection. Do nothing then as Emacs behaves so;
// 2. Indent current line one level right if possible;
// 3. Indent current line to the left-most position if possible;
SelectionModel selectionModel = editor.getSelectionModel();
// Emacs Tab doesn't adjust indent in case of active selection. So do we.
if (selectionModel.hasSelection() || selectionModel.hasBlockSelection()) {
return Result.CONTINUE;
}
// Check if current line is empty. Return eagerly then.
Document document = editor.getDocument();
int caretOffset = editor.getCaretModel().getOffset();
int caretLine = document.getLineNumber(caretOffset);
if (caretLine == 0 || isLineContainsWhiteSpacesOnlyEmpty(document, caretLine)) {
return Result.CONTINUE;
}
ChangeIndentContext context = new ChangeIndentContext(project, file, editor, document, caretLine);
if (defineSoleIndentIfPossible(context)) {
return Result.STOP;
}
switch (tryToIndentToRight(context)) {
case STOP_SUCCESSFUL: return Result.STOP;
case STOP_UNSUCCESSFUL: return Result.CONTINUE;
case CONTINUE: break;
}
if (tryToIndentToLeft(context)) {
return Result.STOP;
}
return Result.CONTINUE;
}
/**
* Checks if target line may have the sole indent (e.g. first expression of compound statement) and applies it in
* case of success.
*
* @param context current processing context
* @return <code>true</code> if target line has the sole indent and it is applied; <code>false</code> otherwise
*/
private static boolean defineSoleIndentIfPossible(@NotNull ChangeIndentContext context) {
int prevLine = context.targetLine - 1;
while (prevLine >= 0 && isLineContainsWhiteSpacesOnlyEmpty(context.document, prevLine)) {
prevLine--;
}
if (prevLine < 0) {
return false;
}
int indent = getLineIndent(context, prevLine);
int newIndent = -1;
if (isLineStartsWithCompoundStatement(context, prevLine)) {
newIndent = indent + context.getIndentOptions().INDENT_SIZE;
}
else if (indent < getLineIndent(context, context.targetLine)) {
newIndent = indent;
}
if (newIndent < 0) {
return false;
}
changeIndent(context, newIndent);
return true;
}
/**
* Tries to indent active line to the right.
*
* @param context current processing context
* @return processing result
*/
private static ProcessingResult tryToIndentToRight(@NotNull ChangeIndentContext context) {
int targetLineIndent = getLineIndent(context, context.targetLine);
List<LineInfo> lineInfos = collectIndentsGreaterOrEqualToCurrent(context, context.targetLine);
int newIndent = -1;
for (int i = lineInfos.size() - 1; i >= 0; i--) {
LineInfo lineInfo = lineInfos.get(i);
if (lineInfo.indent == targetLineIndent) {
continue;
}
newIndent = lineInfo.indent;
break;
}
if (newIndent == targetLineIndent || newIndent < 0) {
return ProcessingResult.CONTINUE;
}
changeIndent(context, newIndent);
return ProcessingResult.STOP_SUCCESSFUL;
}
private static boolean tryToIndentToLeft(@NotNull ChangeIndentContext context) {
if (context.targetLine == 0 || !containsNonWhiteSpaceData(context.document, 0, context.targetLine)) {
changeIndent(context, 0);
return false;
}
int newIndent = -1;
for (int line = 0; line < context.targetLine; line++) {
if (isLineContainsWhiteSpacesOnlyEmpty(context.document, line)) {
continue;
}
int indent = getLineIndent(context, line);
if (isLineStartsWithCompoundStatement(context, line)) {
newIndent = indent;
break;
}
else if (newIndent < 0) {
newIndent = indent;
}
}
if (newIndent < 0) {
return false;
}
changeIndent(context, newIndent);
return true;
}
private static void changeIndent(@NotNull ChangeIndentContext context, int newIndent) {
int caretOffset = context.editor.getCaretModel().getOffset();
String newIndentString = new IndentInfo(0, newIndent, 0).generateNewWhiteSpace(context.getIndentOptions());
int start = context.document.getLineStartOffset(context.targetLine);
int end = DocumentUtil.getFirstNonSpaceCharOffset(context.document, context.targetLine);
context.editor.getDocument().replaceString(start, end, newIndentString);
if (caretOffset > start && caretOffset < end) {
context.editor.getCaretModel().moveToOffset(start + newIndentString.length());
}
}
private static boolean containsNonWhiteSpaceData(@NotNull Document document, int startLine, int endLine) {
CharSequence text = document.getCharsSequence();
int start = document.getLineStartOffset(startLine);
int end = document.getLineStartOffset(endLine);
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c != ' ' && c != '\t' && c != '\n') {
return true;
}
}
return false;
}
/**
* Inspects lines that are located before the given target line and collects information about lines which indents are greater
* or equal to the target line indent.
*
* @param context current processing context
* @param targetLine target line
* @return list of lines located before the target and that start new indented blocks. Note that they are
* stored by their indent size in ascending order
*/
private static List<LineInfo> collectIndentsGreaterOrEqualToCurrent(@NotNull ChangeIndentContext context, int targetLine) {
List<LineInfo> result = new ArrayList<LineInfo>();
int targetLineIndent = getLineIndent(context, targetLine);
int indentUsedLastTime = Integer.MAX_VALUE;
for (int i = targetLine - 1; i >= 0 && indentUsedLastTime > targetLineIndent; i--) {
if (isLineContainsWhiteSpacesOnlyEmpty(context.document, i)) {
continue;
}
int indent = getLineIndent(context, i);
if (indent < targetLineIndent) {
break;
}
if (indent >= indentUsedLastTime) {
continue;
}
PsiElement element = context.file.findElementAt(context.document.getLineStartOffset(i) + indent);
if (element == null) {
continue;
}
ASTNode node = element.getNode();
result.add(new LineInfo(i, indent, COMPOUND_STATEMENT_TYPES.contains(node.getElementType())));
indentUsedLastTime = indent;
}
return result;
}
private static boolean isLineContainsWhiteSpacesOnlyEmpty(@NotNull Document document, int line) {
int start = document.getLineStartOffset(line);
int end = document.getLineEndOffset(line);
CharSequence text = document.getCharsSequence();
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c != ' ' && c != '\t') {
return false;
}
}
return true;
}
private static boolean isLineStartsWithCompoundStatement(@NotNull ChangeIndentContext context, int line) {
PsiElement element = context.file.findElementAt(context.document.getLineStartOffset(line) + getLineIndent(context, line));
if (element == null) {
return false;
}
ASTNode node = element.getNode();
if (node == null) {
return false;
}
return COMPOUND_STATEMENT_TYPES.contains(node.getElementType());
}
private static int getLineIndent(@NotNull ChangeIndentContext context, int line) {
int start = context.document.getLineStartOffset(line);
int end = context.document.getLineEndOffset(line);
int result = 0;
CharSequence text = context.document.getCharsSequence();
for (int i = start; i < end; i++) {
char c = text.charAt(i);
switch (c) {
case ' ': result++; break;
case '\t': result += context.getIndentOptions().TAB_SIZE; break;
default: return result;
}
}
return result;
}
private static class LineInfo {
public final int line;
public final int indent;
public final boolean startsWithCompoundStatement;
LineInfo(int line, int indent, boolean startsWithCompoundStatement) {
this.line = line;
this.indent = indent;
this.startsWithCompoundStatement = startsWithCompoundStatement;
}
@Override
public String toString() {
return "line=" + line + ", indent=" + indent + ", compound=" + startsWithCompoundStatement;
}
}
private static class ChangeIndentContext {
@NotNull public final Project project;
@NotNull public final PsiFile file;
@NotNull public final Editor editor;
@NotNull public final Document document;
public final int targetLine;
private CommonCodeStyleSettings.IndentOptions myIndentOptions;
private ChangeIndentContext(@NotNull Project project, @NotNull PsiFile file, @NotNull Editor editor,
@NotNull Document document, int targetLine)
{
this.project = project;
this.file = file;
this.editor = editor;
this.document = document;
this.targetLine = targetLine;
}
public CommonCodeStyleSettings.IndentOptions getIndentOptions() {
if (myIndentOptions == null) {
CodeStyleSettings codeStyleSettings = CodeStyleSettingsManager.getInstance(project).getCurrentSettings();
myIndentOptions = codeStyleSettings.getIndentOptions(file.getFileType());
}
return myIndentOptions;
}
}
}