/*
* Copyright 2013-2017 consulo.io
*
* 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 consulo.csharp.lang.doc.ide.codeInsight.editorActions;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.intellij.openapi.diagnostic.Logger;
import consulo.csharp.lang.doc.CSharpDocUtil;
import consulo.csharp.lang.doc.psi.CSharpDocAttributeValue;
import consulo.csharp.lang.doc.psi.CSharpDocTag;
import consulo.csharp.lang.doc.psi.CSharpDocText;
import consulo.csharp.lang.doc.psi.CSharpDocTokenType;
import consulo.csharp.lang.psi.CSharpFile;
import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.FileViewProvider;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.TokenType;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.RoleFinder;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Function;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import consulo.annotations.RequiredDispatchThread;
public class CSharpDocGtTypedHandler extends TypedHandlerDelegate
{
private static final Logger LOGGER = Logger.getInstance(CSharpDocGtTypedHandler.class);
private static final RoleFinder CLOSING_TAG_NAME_FINDER = new RoleFinder()
{
@Override
@Nullable
public ASTNode findChild(@NotNull ASTNode parent)
{
final PsiElement element = getEndTagNameElement((CSharpDocTag) parent.getPsi());
return element == null ? null : element.getNode();
}
};
@Override
@RequiredDispatchThread
public Result beforeCharTyped(final char c, final Project project, final Editor editor, final PsiFile editedFile, final FileType fileType)
{
if(!(editedFile instanceof CSharpFile))
{
return Result.CONTINUE;
}
if(c == '>')
{
PsiDocumentManager.getInstance(project).commitAllDocuments();
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
FileViewProvider provider = editedFile.getViewProvider();
int offset = editor.getCaretModel().getOffset();
PsiElement element, elementAtCaret = null;
if(offset < editor.getDocument().getTextLength())
{
elementAtCaret = element = provider.findElementAt(offset);
if(!(element instanceof PsiWhiteSpace))
{
boolean nonAcceptableDelimiter = true;
if(element != null)
{
IElementType tokenType = getElementType(element);
if(tokenType == CSharpDocTokenType.XML_START_TAG_START || tokenType == CSharpDocTokenType.XML_END_TAG_START)
{
if(offset > 0)
{
PsiElement previousElement = provider.findElementAt(offset - 1);
if(previousElement != null)
{
tokenType = getElementType(previousElement);
element = previousElement;
nonAcceptableDelimiter = false;
}
}
}
else if(tokenType == CSharpDocTokenType.XML_NAME)
{
if(element.getNextSibling() instanceof PsiErrorElement)
{
nonAcceptableDelimiter = false;
}
}
if(tokenType == CSharpDocTokenType.XML_TAG_END || tokenType == CSharpDocTokenType.XML_EMPTY_ELEMENT_END && element
.getTextOffset() == offset - 1)
{
editor.getCaretModel().moveToOffset(offset + 1);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
return Result.STOP;
}
}
if(nonAcceptableDelimiter)
{
return Result.CONTINUE;
}
}
else
{
// check if right after empty end
PsiElement previousElement = provider.findElementAt(offset - 1);
if(previousElement != null)
{
final IElementType tokenType = getElementType(previousElement);
if(tokenType == CSharpDocTokenType.XML_EMPTY_ELEMENT_END)
{
return Result.STOP;
}
}
}
PsiElement parent = element.getParent();
if(parent instanceof CSharpDocText)
{
final String text = parent.getText();
// check /
final int index = offset - parent.getTextOffset() - 1;
if(index >= 0 && text.charAt(index) == '/')
{
return Result.CONTINUE; // already seen /
}
element = parent.getPrevSibling();
}
else if(parent instanceof CSharpDocTag && !(element.getPrevSibling() instanceof CSharpDocTag))
{
element = parent;
}
else if(parent instanceof CSharpDocAttributeValue)
{
element = parent;
}
}
else
{
element = provider.findElementAt(editor.getDocument().getTextLength() - 1);
if(element == null)
{
return Result.CONTINUE;
}
element = element.getParent();
}
if(element instanceof CSharpDocAttributeValue)
{
element = element.getParent().getParent();
}
while(element instanceof PsiWhiteSpace)
{
element = element.getPrevSibling();
}
if(element == null)
{
return Result.CONTINUE;
}
if(!CSharpDocUtil.isInsideDoc(element))
{
return Result.CONTINUE;
}
if(!(element instanceof CSharpDocTag))
{
if(element.getPrevSibling() != null &&
element.getPrevSibling().getText().equals("<"))
{
// tag is started and there is another text in the end
editor.getDocument().insertString(offset, "</" + element.getText() + ">");
}
return Result.CONTINUE;
}
CSharpDocTag tag = (CSharpDocTag) element;
if(getTokenOfType(tag, CSharpDocTokenType.XML_TAG_END) != null)
{
return Result.CONTINUE;
}
if(getTokenOfType(tag, CSharpDocTokenType.XML_EMPTY_ELEMENT_END) != null)
{
return Result.CONTINUE;
}
final PsiElement startToken = getTokenOfType(tag, CSharpDocTokenType.XML_START_TAG_START);
if(startToken == null || !startToken.getText().equals("<"))
{
return Result.CONTINUE;
}
String name = tag.getName();
if(elementAtCaret != null && getElementType(elementAtCaret) == CSharpDocTokenType.XML_NAME)
{
name = name.substring(0, offset - elementAtCaret.getTextOffset());
}
if(StringUtil.isEmpty(name))
{
return Result.CONTINUE;
}
int tagOffset = tag.getTextRange().getStartOffset();
final PsiElement nameToken = getTokenOfType(tag, CSharpDocTokenType.XML_NAME);
if(nameToken != null && nameToken.getTextRange().getStartOffset() > offset)
{
return Result.CONTINUE;
}
HighlighterIterator iterator = ((EditorEx) editor).getHighlighter().createIterator(tagOffset);
if(BraceMatchingUtil.matchBrace(editor.getDocument().getCharsSequence(), editedFile.getFileType(), iterator, true, true))
{
PsiElement parent = tag.getParent();
boolean hasBalance = true;
while(parent instanceof CSharpDocTag && name.equals(((CSharpDocTag) parent).getName()))
{
ASTNode astNode = CLOSING_TAG_NAME_FINDER.findChild(parent.getNode());
if(astNode == null)
{
hasBalance = false;
break;
}
parent = parent.getParent();
}
if(hasBalance)
{
hasBalance = false;
for(ASTNode node = parent.getNode().getLastChildNode(); node != null; node = node.getTreePrev())
{
ASTNode leaf = node;
if(leaf.getElementType() == TokenType.ERROR_ELEMENT)
{
ASTNode firstChild = leaf.getFirstChildNode();
if(firstChild != null)
{
leaf = firstChild;
}
else
{
PsiElement psiElement = PsiTreeUtil.nextLeaf(leaf.getPsi());
leaf = psiElement != null ? psiElement.getNode() : null;
}
if(leaf != null && leaf.getElementType() == TokenType.WHITE_SPACE)
{
PsiElement psiElement = PsiTreeUtil.nextLeaf(leaf.getPsi());
if(psiElement != null)
{
leaf = psiElement.getNode();
}
}
}
if(leaf != null && leaf.getElementType() == CSharpDocTokenType.XML_END_TAG_START)
{
ASTNode treeNext = leaf.getTreeNext();
IElementType treeNextType;
if(treeNext != null && ((treeNextType = treeNext.getElementType()) == CSharpDocTokenType.XML_NAME || treeNextType ==
CSharpDocTokenType.XML_TAG_NAME))
{
if(name.equals(treeNext.getText()))
{
ASTNode parentEndName = parent instanceof CSharpDocTag ? CLOSING_TAG_NAME_FINDER.findChild(parent.getNode()) :
null;
hasBalance = !(parent instanceof CSharpDocTag) || parentEndName != null && !parentEndName.getText().equals(name);
break;
}
}
}
}
}
if(hasBalance)
{
return Result.CONTINUE;
}
}
TextRange cdataReformatRange = null;
editor.getDocument().insertString(offset, "</" + name + ">");
if(cdataReformatRange != null)
{
PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument());
try
{
CodeStyleManager.getInstance(project).reformatText(file, cdataReformatRange.getStartOffset(), cdataReformatRange.getEndOffset());
}
catch(IncorrectOperationException e)
{
CSharpDocGtTypedHandler.LOGGER.error(e);
}
}
return cdataReformatRange != null ? Result.STOP : Result.CONTINUE;
}
return Result.CONTINUE;
}
@Nullable
public static PsiElement getEndTagNameElement(@NotNull CSharpDocTag tag)
{
final ASTNode node = tag.getNode();
if(node == null)
{
return null;
}
ASTNode current = node.getLastChildNode();
ASTNode prev = current;
while(current != null)
{
final IElementType elementType = prev.getElementType();
if((elementType == CSharpDocTokenType.XML_NAME || elementType == CSharpDocTokenType.XML_TAG_NAME) && current.getElementType() ==
CSharpDocTokenType.XML_END_TAG_START)
{
return prev.getPsi();
}
prev = current;
current = current.getTreePrev();
}
return null;
}
public static IElementType getElementType(PsiElement element)
{
ASTNode node = element.getNode();
return node == null ? null : node.getElementType();
}
@Nullable
public static PsiElement getTokenOfType(PsiElement element, IElementType type)
{
if(element == null)
{
return null;
}
List<PsiElement> map = ContainerUtil.map(element.getNode().getChildren(TokenSet.create(type)), new Function<ASTNode, PsiElement>()
{
@Override
public PsiElement fun(ASTNode astNode)
{
return astNode.getPsi();
}
});
return ContainerUtil.getFirstItem(map);
}
}