package com.siberika.idea.pascal.editor.formatter;
import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.siberika.idea.pascal.lang.psi.PasAssignPart;
import com.siberika.idea.pascal.lang.psi.PasCallExpr;
import com.siberika.idea.pascal.lang.psi.PasEntityScope;
import com.siberika.idea.pascal.lang.psi.PasExpression;
import com.siberika.idea.pascal.lang.psi.PasForStatement;
import com.siberika.idea.pascal.lang.psi.PasFromExpression;
import com.siberika.idea.pascal.lang.psi.PasFullyQualifiedIdent;
import com.siberika.idea.pascal.lang.psi.PasIfStatement;
import com.siberika.idea.pascal.lang.psi.PasParenExpr;
import com.siberika.idea.pascal.lang.psi.PasRepeatStatement;
import com.siberika.idea.pascal.lang.psi.PasStatement;
import com.siberika.idea.pascal.lang.psi.PasTypes;
import com.siberika.idea.pascal.lang.psi.PasWhileStatement;
import com.siberika.idea.pascal.lang.psi.PascalPsiElement;
import com.siberika.idea.pascal.lang.psi.PascalVariableDeclaration;
import com.siberika.idea.pascal.lang.psi.impl.PascalExpression;
import com.siberika.idea.pascal.util.DocUtil;
import com.siberika.idea.pascal.util.EditorUtil;
import com.siberika.idea.pascal.util.PsiUtil;
import org.jetbrains.annotations.NotNull;
/**
* Author: George Bakhtadze
* Date: 17/02/2015
*/
public class PascalSmartEnterProcessor extends SmartEnterProcessor {
@Override
public boolean process(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile psiFile) {
FeatureUsageTracker.getInstance().triggerFeatureUsed("codeassists.complete.statement");
PsiElement atCursor = getStatementAtCaret(editor, psiFile);
PascalPsiElement el = PsiTreeUtil.getParentOfType(atCursor, PasStatement.class, PasEntityScope.class, PascalExpression.class);
if ((el instanceof PasStatement) || (el instanceof PascalExpression)) {
completeStatement(editor, el);
} else {
el = PsiTreeUtil.getParentOfType(atCursor, PascalVariableDeclaration.class);
if (el != null) {
PascalCompleteIdent.completeIdent(editor, (PascalVariableDeclaration) el);
}
}
if (el != null) {
CodeStyleManager.getInstance(el.getManager()).reformat(el, true);
}
return true;
}
private static void completeStatement(Editor editor, PsiElement statement) {
/** complete if, for, while, repeat, try, with
* complete ";" to EOL if needed
* complete ")" in calls where needed
* complete "end" if statement contains "begin" and existing "end" has less indent
* complete routines
* complete structured types
* remove ";" at EOL after "begin"
*/
boolean processParent = false;
if (statement instanceof PasWhileStatement) {
completeWhile(editor, (PasWhileStatement) statement);
} else if (statement instanceof PasForStatement) {
completeFor(editor, (PasForStatement) statement);
} else if (statement instanceof PasIfStatement) {
completeIf(editor, (PasIfStatement) statement);
} else if (statement instanceof PasRepeatStatement) {
completeRepeat(editor, (PasRepeatStatement) statement);
} else if ((statement instanceof PasCallExpr) || (statement instanceof PasParenExpr)) {
processParent = !completeParen(editor, (PascalExpression) statement);
} else if (statement instanceof PasStatement) {
processParent = !completeSimpleStatement(editor, (PasStatement) statement);
} else {
processParent = true;
}
if (processParent) {
if (statement instanceof PasStatement) {
int ofs = statement.getTextRange().getEndOffset();
ofs = DocUtil.expandRangeEnd(editor.getDocument(), ofs, DocUtil.RE_SEMICOLON);
editor.getCaretModel().moveToOffset(ofs);
} else {
PascalPsiElement parent = PsiTreeUtil.getParentOfType(statement, PasStatement.class, PascalExpression.class);
if (parent != null) {
completeStatement(editor, parent);
}
}
}
}
private static boolean completeSimpleStatement(Editor editor, PasStatement outerStatement) {
if (!DocUtil.isSingleLine(editor.getDocument(), outerStatement)) {
PascalPsiElement stmt = PsiUtil.findImmChildOfAnyType(outerStatement, PasAssignPart.class);
stmt = stmt != null ? stmt : PsiUtil.findImmChildOfAnyType(outerStatement, PasAssignPart.class, PasExpression.class);
if (stmt != null) {
int endStmt = stmt.getTextRange().getEndOffset();
if (endStmt != outerStatement.getTextRange().getEndOffset()) {
DocUtil.adjustDocument(editor, endStmt, String.format(";%s", DocUtil.PLACEHOLDER_CARET));
return true;
}
}
}
return false;
}
/* WHILE
. while [do] => while _ do
. while expr [do] => while expr do \n _
. while expr do _ => while expr do \n begin \n _ \n end;
. while expr do _ \n stmt1; => while expr do \n _; stmt1;
*/
private static final int LENGTH_WHILE = "while".length();
private static void completeWhile(Editor editor, PasWhileStatement statement) {
int doEnd = getChildEndOffset(statement, PasTypes.DO);
final PasExpression expr = statement.getExpression();
boolean hasExpr = (expr != null) && atTheSameLine(editor.getDocument(), statement, expr);
if (hasExpr) {
if (doEnd >= 0) {
if (editor.getCaretModel().getCurrentCaret().getOffset() == doEnd) {
DocUtil.adjustDocument(editor, doEnd, String.format("\nbegin\n%s\nend;", DocUtil.PLACEHOLDER_CARET));
} else {
DocUtil.adjustDocument(editor, doEnd, String.format("\n%s", DocUtil.PLACEHOLDER_CARET));
}
} else {
int offs = expr.getTextRange().getEndOffset();
DocUtil.adjustDocument(editor, offs, String.format(" do \n%s", DocUtil.PLACEHOLDER_CARET));
}
EditorUtil.moveToLineEnd(editor);
} else {
int offs = statement.getTextRange().getStartOffset() + LENGTH_WHILE;
DocUtil.adjustDocument(editor, offs, " " + DocUtil.PLACEHOLDER_CARET + (doEnd >= 0 ? "" : " do"));
}
PsiDocumentManager.getInstance(statement.getProject()).commitDocument(editor.getDocument());
}
/* IF
* if => if _ then
* if expr [then] => if expr then \n _;
* if expr then _ => if expr then \n begin \n _ \n end;
* if expr then stmt _ => if expr then stmt else _
* if expr then [begin] stmts [end] _ => if expr then [begin] stmts [end] ?\n else _
* if expr then [begin] stmts [end] else _ => if expr then begin stmts end else \n begin \n _ \n end;
*/
private static final int LENGTH_IF = "if".length();
private static void completeIf(Editor editor, PasIfStatement statement) {
int thenEnd = getChildEndOffset(statement, PasTypes.THEN);
final PasExpression expr = statement.getExpression();
boolean hasExpr = (expr != null) && atTheSameLine(editor.getDocument(), statement, expr);
if (hasExpr) {
if (thenEnd >= 0) {
int caretPos = editor.getCaretModel().getCurrentCaret().getOffset();
int elseEnd = getChildEndOffset(statement, PasTypes.ELSE);
TextRange thenStmtRange = getElementRange(statement.getIfThenStatement());
TextRange elseStmtRange = getElementRange(statement.getIfElseStatement());
if ((thenStmtRange.getLength() > 0) && (elseEnd < 0) && (caretPos == thenStmtRange.getEndOffset())) {
DocUtil.adjustDocument(editor, caretPos, String.format("\nelse\n%s", DocUtil.PLACEHOLDER_CARET));
} else if ((caretPos == thenEnd) && (thenStmtRange.getLength() == 0)) {
DocUtil.adjustDocument(editor, caretPos, String.format("\nbegin\n%s\nend", DocUtil.PLACEHOLDER_CARET));
} else if ((caretPos == elseEnd) && (elseStmtRange.getLength() == 0)) {
DocUtil.adjustDocument(editor, caretPos, String.format("\nbegin\n%s\nend", DocUtil.PLACEHOLDER_CARET));
} else if (caretPos < thenEnd) {
DocUtil.adjustDocument(editor, thenEnd, String.format("\n%s", DocUtil.PLACEHOLDER_CARET));
} else if (caretPos < elseEnd) {
DocUtil.adjustDocument(editor, elseEnd, String.format("\n%s", DocUtil.PLACEHOLDER_CARET));
}
} else {
int offs = expr.getTextRange().getEndOffset();
DocUtil.adjustDocument(editor, offs, String.format(" then \n%s", DocUtil.PLACEHOLDER_CARET));
}
EditorUtil.moveToLineEnd(editor);
} else {
int offs = statement.getTextRange().getStartOffset() + LENGTH_IF;
DocUtil.adjustDocument(editor, offs, " " + DocUtil.PLACEHOLDER_CARET + (thenEnd >= 0 ? "" : " then"));
}
PsiDocumentManager.getInstance(statement.getProject()).commitDocument(editor.getDocument());
}
/* REPEAT
* repeat => repeat \n _ \n until
* repeat until [;] => repeat \n until _;
*/
private static final int LENGTH_REPEAT = "repeat".length();
private static void completeRepeat(Editor editor, PasRepeatStatement statement) {
int untilEnd = getChildEndOffset(statement, PasTypes.UNTIL);
if (untilEnd < 0) {
DocUtil.adjustDocument(editor, statement.getTextRange().getStartOffset() + LENGTH_REPEAT,
String.format("\n%s\nuntil;", DocUtil.PLACEHOLDER_CARET));
EditorUtil.moveToLineEnd(editor);
} else if (statement.getExpression() == null) {
DocUtil.adjustDocument(editor, untilEnd, String.format(" %s;", DocUtil.PLACEHOLDER_CARET));
}
PsiDocumentManager.getInstance(statement.getProject()).commitDocument(editor.getDocument());
}
private static TextRange getElementRange(PsiElement element) {
return element != null ? element.getTextRange() : TextRange.EMPTY_RANGE;
}
/* FOR
* for => for _ do
* for i [to] [do] => for i := _ [to] [do] // if i is 1-letter in length
* for [i] to [do] => for i := _ to [do]
* for i := [to] [do] => for i := _ to do
* for i := 0 [to] [do] => for i := 0 to _ do
* for i := 0 to 1 [do] => for i := 0 to 1 do \n _
* for in [do] => for _ in [do]
* for i in [do] => for i in _ do
* for i in x [do] => for i in x do \n _
? for x [do] => for i in x do \n _ // for enumerable types of x
*/
private static final int LENGTH_FOR = "for".length();
private static void completeFor(Editor editor, PasForStatement statement) {
int assignEnd = getChildEndOffset(PasTypes.ASSIGN, statement, statement.getStatement());
int inEnd = getChildEndOffset(PasTypes.IN, statement, statement.getStatement());
PsiElement errorEl = PsiTreeUtil.findChildOfType(statement.getStatement(), PsiErrorElement.class);
int toEnd = getChildEndOffset(PasTypes.TO, statement, statement.getStatement(), errorEl);
toEnd = toEnd < 0 ? getChildEndOffset(PasTypes.DOWNTO, statement, statement.getStatement(), errorEl) : toEnd;
int doEnd = getChildEndOffset(PasTypes.DO, statement, statement.getStatement());
final PasFullyQualifiedIdent ident = statement.getFullyQualifiedIdent();
boolean hasIdent = (ident != null) && atTheSameLine(editor.getDocument(), statement, ident);
final PasFromExpression fromExpr = statement.getFromExpression();
final PasExpression expr = statement.getExpression();
boolean hasExpr = (expr != null) && atTheSameLine(editor.getDocument(), statement, expr);
String doStr = doEnd >= 0 ? "" : " do";
if (hasIdent) {
String toStr = toEnd >= 0 ? "" : " to";
if (assignEnd < 0) {
if (inEnd < 0) {
if ((ident.getTextLength() == 1) || (toEnd >= 0)) {
DocUtil.adjustDocument(editor, ident.getTextRange().getEndOffset(), " := " + DocUtil.PLACEHOLDER_CARET + toStr + doStr);
}
} else if (!hasExpr) {
DocUtil.adjustDocument(editor, inEnd, " " + DocUtil.PLACEHOLDER_CARET + doStr);
} else {
insertNewLine(editor, doStr, doEnd >= 0 ? doEnd : expr.getTextRange().getEndOffset());
}
} else if (null == fromExpr) {
DocUtil.adjustDocument(editor, assignEnd, " " + DocUtil.PLACEHOLDER_CARET + toStr + doStr);
} else if (toEnd < 0) {
DocUtil.adjustDocument(editor, fromExpr.getTextRange().getEndOffset(), " to " + DocUtil.PLACEHOLDER_CARET + doStr);
} else if (!hasExpr) {
DocUtil.adjustDocument(editor, toEnd, " " + DocUtil.PLACEHOLDER_CARET + doStr);
} else {
insertNewLine(editor, doStr, doEnd >= 0 ? doEnd : expr.getTextRange().getEndOffset());
}
} else {
int offs = statement.getTextRange().getStartOffset() + LENGTH_FOR;
DocUtil.adjustDocument(editor, offs, " " + DocUtil.PLACEHOLDER_CARET + (inEnd < 0 ? doStr : ""));
}
PsiDocumentManager.getInstance(statement.getProject()).commitDocument(editor.getDocument());
}
/* EXPRESSION
* ([x] => ([x]_)
* func([x] => func([x]_)
*/
private static boolean completeParen(Editor editor, PascalExpression statement) {
int parenPos = getChildEndOffset(PasTypes.RPAREN, statement, statement.getLastChild());
if (parenPos < 0) {
int ofs = DocUtil.isSingleLine(editor.getDocument(), statement) ? statement.getTextRange().getEndOffset() : getVisualLineEnd(editor) - 1;
DocUtil.adjustDocument(editor, ofs, DocUtil.PLACEHOLDER_CARET + ")");
PsiDocumentManager.getInstance(statement.getProject()).commitDocument(editor.getDocument());
return true;
}
return false;
}
private static void insertNewLine(Editor editor, String doStr, int offset) {
DocUtil.adjustDocument(editor, offset, doStr + "\n" + DocUtil.PLACEHOLDER_CARET);
int caretPos = editor.getCaretModel().getCurrentCaret().getOffset();
int lineEndPos = getVisualLineEnd(editor) - 1;
if (caretPos >= lineEndPos) {
EditorUtil.moveToLineEnd(editor);
}
}
private static int getVisualLineEnd(Editor editor) {
return editor.getCaretModel().getCurrentCaret().getVisualLineEnd();
}
private static int getChildEndOffset(PsiElement element, IElementType type) {
ASTNode child = element.getNode().findChildByType(type);
return child != null ? child.getTextRange().getEndOffset() : -1;
}
static int getChildEndOffset(IElementType type, PsiElement...elements) {
for (PsiElement element : elements) if (element != null) {
ASTNode child = element.getNode().findChildByType(type);
if (child != null) {
return child.getTextRange().getEndOffset();
}
}
return -1;
}
private static boolean atTheSameLine(Document doc, PsiElement first, PsiElement second) {
return doc.getLineNumber(first.getTextRange().getStartOffset()) == doc.getLineNumber(second.getTextRange().getStartOffset());
}
}